Revoke the access grant that was actually clicked (fixes #126) #132

Merged
qwc merged 2 commits from fix/revoke-nonmanual-access into main 2026-08-30 18:03:47 +02:00
Owner

Fixes #126 — "User access cannot be revoked, click on the button does not do anything".

Root cause

The markup and the handler both looked fine, which is why this was hard to spot. The mismatch is between what the page lists and what the store deletes:

  • ProjectAccessStore.ListByProject returns every project_access row for the project, whatever its source, and the edit page renders all of them.
  • The Revoke form posted only user_id, and ProjectAccessStore.Revoke deletes WHERE project_id = ? AND user_id = ? AND source = 'manual'.

So revoking a user whose access came from an LDAP or OAuth2 group mapping deleted zero rows. The POST succeeded, the handler redirected 303, the page reloaded with the row still in the table — indistinguishable from a dead button.

The fix

  • The Revoke form carries the row's source in a hidden field; handleAdminRevokeAccess revokes that exact grant via the existing RevokeBySource. A missing field still means manual, so nothing that predates the field changes behaviour.
  • The source is validated against the set the application actually records (database.ValidAccessSource), so the field can't widen into a free-form delete filter.
  • The access table gained a Source column, and a note appears when synced grants are present: revoking one works immediately, but the user regains it at their next login unless the group mapping changes.
  • internal/docs/builtin/docs/reference/roles-permissions.md documents the grant sources and that revoke/re-sync interaction.

Tests

internal/handler/revoke_access_test.go drives the real form POST end to end. TestRevokeRemovesAccessRegardlessOfSource fails on main for the ldap and oauth2 subtests (still have 1 row(s)) and passes for manual — the reported symptom, reproduced. Also covers source-scoped deletion and rejection of an unknown source. Full suite green.

Follow-up found while testing (not in this PR)

On SQLite only, migration 002_auth_groups never dropped the original UNIQUE(project_id, user_id) table constraint — DROP INDEX IF EXISTS idx_project_access_unique is a no-op against SQLite's sqlite_autoindex_project_access_1, which cannot be dropped that way. Postgres and MySQL drop theirs correctly. A separate issue follows; it needs a table-rebuild migration and deserves its own review.


Assisted by Claude Opus 5.

Fixes #126 — "User access cannot be revoked, click on the button does not do anything". ## Root cause The markup and the handler both looked fine, which is why this was hard to spot. The mismatch is between what the page *lists* and what the store *deletes*: - `ProjectAccessStore.ListByProject` returns every `project_access` row for the project, whatever its `source`, and the edit page renders all of them. - The Revoke form posted only `user_id`, and `ProjectAccessStore.Revoke` deletes `WHERE project_id = ? AND user_id = ? AND source = 'manual'`. So revoking a user whose access came from an LDAP or OAuth2 group mapping deleted zero rows. The POST succeeded, the handler redirected 303, the page reloaded with the row still in the table — indistinguishable from a dead button. ## The fix - The Revoke form carries the row's `source` in a hidden field; `handleAdminRevokeAccess` revokes that exact grant via the existing `RevokeBySource`. A missing field still means `manual`, so nothing that predates the field changes behaviour. - The source is validated against the set the application actually records (`database.ValidAccessSource`), so the field can't widen into a free-form delete filter. - The access table gained a **Source** column, and a note appears when synced grants are present: revoking one works immediately, but the user regains it at their next login unless the group mapping changes. - `internal/docs/builtin/docs/reference/roles-permissions.md` documents the grant sources and that revoke/re-sync interaction. ## Tests `internal/handler/revoke_access_test.go` drives the real form POST end to end. `TestRevokeRemovesAccessRegardlessOfSource` fails on `main` for the `ldap` and `oauth2` subtests (`still have 1 row(s)`) and passes for `manual` — the reported symptom, reproduced. Also covers source-scoped deletion and rejection of an unknown source. Full suite green. ## Follow-up found while testing (not in this PR) On SQLite only, migration `002_auth_groups` never dropped the original `UNIQUE(project_id, user_id)` table constraint — `DROP INDEX IF EXISTS idx_project_access_unique` is a no-op against SQLite's `sqlite_autoindex_project_access_1`, which cannot be dropped that way. Postgres and MySQL drop theirs correctly. A separate issue follows; it needs a table-rebuild migration and deserves its own review. --- Assisted by Claude Opus 5.
Revoke the access grant that was actually clicked (fixes #126)
Some checks failed
CI / build (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
7682590180
The project edit page lists every project_access row whatever its
source, but the Revoke button posted only a user_id and the store's
Revoke deleted `WHERE source = 'manual'`. Revoking a user whose access
came from an LDAP or OAuth2 group mapping deleted nothing: the request
redirected back 303 and the row was still there, so the button looked
dead.

The form now carries the row's source and the handler revokes that
exact grant, rejecting any source the application doesn't record. The
table gained a Source column so it's visible where a grant came from,
plus a note that synced grants return at the user's next login unless
the group mapping changes.

Assisted-by: Claude Opus 5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marcel M. Otte <marcel.otte@mmo.to>
Point the revoke test comment at issue #133
All checks were successful
CI / test (pull_request) Successful in 1m20s
CI / build (pull_request) Successful in 56s
CI / docker (pull_request) Has been skipped
2b9af33b87
Assisted-by: Claude Opus 5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marcel M. Otte <marcel.otte@mmo.to>
qwc merged commit 7371d1f749 into main 2026-08-30 18:03:47 +02:00
qwc deleted branch fix/revoke-nonmanual-access 2026-08-30 18:03:47 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
qwc-open/asiakirjat!132
No description provided.