Public Access
feat(admin): let admins change user roles
V32 adds role_changed_at and the role_change log. Role changes lock the admin rows, so there is always one admin left; the last admin cannot delete their own account. Login and refresh read the role under the user lock. Tokens issued before a role change are refused.
This commit is contained in:
1 parent
3410cbc598
commit
365b69feb4
44 files changed
+1238
-75
No files matched your search
@@ -60,6 +60,14 @@ To Be Played is split into three repositories:
|
||||
down to USER or AUDITOR and back (`POST /api/auth/view-as`); the choice is stored per login in
|
||||
`refresh_tokens.active_role` and never goes above the stored role, so a demoted admin loses the rights at the next
|
||||
refresh. Core checks the same claim.
|
||||
**Role change**: an admin sets the role of another user on the admin users page (`PUT /api/admin/users/{id}/role`).
|
||||
The change is a row in `role_change` (actor, old and new role, time; shown in the "Roles" dialog) and in
|
||||
`admin_audit` (`ROLE_CHANGE`), sets `users.role_changed_at` and clears "view as" of the open logins. An access
|
||||
token issued before `role_changed_at` counts as not logged in (`401`) at Auth and at Core, within 30 s (cache
|
||||
TTL); the frontend then refreshes and repeats the request, and the refresh reads the stored role. A role set by
|
||||
SQL (the first admin) sets no `role_changed_at`; it works from the next refresh. There is never 0 admins: every
|
||||
write that can remove an admin first locks the active admins (`shared/account/ActiveAdmins`, `SELECT ... FOR
|
||||
UPDATE`), the actor must still be one of them, and the last admin cannot delete the own account.
|
||||
- **Role markers on services**: every service method a controller calls carries `@RequiresRole(Role.X)` or
|
||||
`@Public`, on the method or on its class (the method wins). A Spring Security method interceptor checks the
|
||||
active role while a request is served (scheduled jobs and `@Async` work run without a user and are not checked);
|
||||
@@ -68,8 +76,9 @@ To Be Played is split into three repositories:
|
||||
guard. ArchUnit fails the build for an unmarked method, two markers, a controller calling an `@Async` method, and a
|
||||
service method calling another service's method that needs more.
|
||||
- **Admin users page** (`/api/admin/**`, active role PLATFORM_ADMIN): search and page all users, block and unblock
|
||||
(`users.enabled`; blocking ends every login and, within 30 s, the access token at Auth; Core accepts the token
|
||||
until it expires, max 15 min; the login then answers `403 ACCOUNT_BLOCKED`, but only after the right password), delete (the same flag and nightly cleanup as "Delete my account"). No action on the own account
|
||||
(`users.enabled`; blocking ends every login and, within 30 s, the access token at Auth and at Core; the login then
|
||||
answers `403 ACCOUNT_BLOCKED`, but only after the right password), delete (the same flag and nightly cleanup as
|
||||
"Delete my account"). No action on the own account
|
||||
or on another admin. Every action and every "view as" is a row in `admin_audit`.
|
||||
Accept or deny an account that waits (`users.status` PENDING; the login answers `403 ACCOUNT_PENDING` after the
|
||||
right password): accept makes it ACTIVE and mails "your account is active"; deny makes it DENIED, mails the user
|
||||
@@ -132,6 +141,8 @@ To Be Played is split into three repositories:
|
||||
| `GET` | `/api/admin/users?query=&status=&page=&size=` (`{content, page, size, totalElements, totalPages}`; `status` UNCONFIRMED, PENDING, ACTIVE or DENIED, empty for all; each user has `status`) | PLATFORM_ADMIN |
|
||||
| `GET` | `/api/admin/users/pending-count` (`{count}`: registrations that wait) | PLATFORM_ADMIN |
|
||||
| `PUT` | `/api/admin/users/{id}/decision` (`{accepted}`; `204`; `409 NOT_PENDING`, also for an unconfirmed request) | PLATFORM_ADMIN |
|
||||
| `PUT` | `/api/admin/users/{id}/role` (`{role, expectedRole}`; `204`; `400` for an unknown role; `404 USER_NOT_FOUND`; `409 SELF_ACTION`, `ACTOR_NOT_ADMIN`, `ROLE_CHANGED` (the role is not `expectedRole`), `ALREADY_DELETED`, `TARGET_IS_BLOCKED`, `TARGET_IS_PENDING`, `TARGET_IS_UNCONFIRMED`) | PLATFORM_ADMIN |
|
||||
| `GET` | `/api/admin/users/{id}/role-changes` (`{results: [{actorUserId, actorEmail, oldRole, newRole, changedAt}]}`, newest first, 50 at most; `actorEmail` null after the actor is purged) | PLATFORM_ADMIN |
|
||||
| `PUT` | `/api/admin/users/{id}/blocked` (`{blocked}`; `409 SELF_ACTION`, `TARGET_IS_ADMIN`, `ALREADY_DELETED`, `TARGET_IS_PENDING`) | PLATFORM_ADMIN |
|
||||
| `DELETE` | `/api/admin/users/{id}` (`409 SELF_ACTION`, `TARGET_IS_ADMIN`, `TARGET_IS_PENDING`, `TARGET_IS_UNCONFIRMED`) | PLATFORM_ADMIN |
|
||||
| `DELETE` | `/api/admin/users/{id}/unconfirmed` (hard delete of an unconfirmed request; `204`; `404`; `409 NOT_UNCONFIRMED`, `SELF_ACTION`, `TARGET_IS_ADMIN`) | PLATFORM_ADMIN |
|
||||
@@ -150,7 +161,7 @@ To Be Played is split into three repositories:
|
||||
| `POST` | `/api/admin/news` (`{titleEn, titleDe, textEn, textDe}`; `201 {id}`) | PLATFORM_ADMIN |
|
||||
| `PUT` | `/api/admin/news/{id}` (same body; `204`; `404 NEWS_NOT_FOUND`) | PLATFORM_ADMIN |
|
||||
| `DELETE` | `/api/admin/news/{id}` (`204`; `404 NEWS_NOT_FOUND`) | PLATFORM_ADMIN |
|
||||
| `DELETE` | `/api/users/me` (`{password}`; `204` and both cookies cleared, `403 INVALID_PASSWORD`, `429`) | access token |
|
||||
| `DELETE` | `/api/users/me` (`{password}`; `204` and both cookies cleared, `403 INVALID_PASSWORD`, `409 LAST_ADMIN` (the only active admin), `429`) | access token |
|
||||
| `GET` | `/api/steam-link/callback` | public, Steam's redirect back; binding cookie; redirects to the frontend settings |
|
||||
| `PUT` | `/api/users/settings/appearance` | access token |
|
||||
| `PUT` | `/api/users/settings/profile-style` (`{accent, corners, bannerAppid, backgroundAppid}`; `204`) | access token |
|
||||
@@ -194,9 +205,9 @@ may log in and is shown to others" uses `shared/account/ActiveUser`.
|
||||
A settings change is one `UserRepository.update(userId, change)`: load the user, change the model, write back the
|
||||
display name and the settings. A new setting needs no new repository method.
|
||||
Three Caffeine caches in `CacheConfig`: `users` (by userId) and `publicStats` expire 10 minutes after the write,
|
||||
`activeUsers` (the filter's "blocked or deleted?" check per request) after 30 seconds. Every write evicts what it
|
||||
changes. A request that read the database just before a block or delete can put the old answer back, so the short
|
||||
TTL is the real limit: a blocked or deleted user keeps a valid access token for up to 30 s.
|
||||
`activeUsers` (the filter's "blocked or deleted, and when did the role change?" check per request) after 30 seconds.
|
||||
Every write evicts what it changes. A request that read the database just before a block or delete can put the old answer back, so the short
|
||||
TTL is the real limit: a blocked or deleted user, or a token from before a role change, works for up to 30 s.
|
||||
`profile` and `stats` only read data. They have no entities and use `JdbcTemplate` queries in a `repo` package.
|
||||
`version` has only an `api` package: `GET /api/version` answers the commit and build time of the running image.
|
||||
`account` (delete and nightly cleanup) sets the flag with `JdbcTemplate` in a `repo` package too, and calls Core
|
||||
@@ -391,8 +402,8 @@ sequenceDiagram
|
||||
participant A as Auth
|
||||
participant C as Core
|
||||
A->>A: link / unlink / block / unblock / name change (commit)
|
||||
A->>C: POST /internal/accounts/states {userId, steamId, displayName, blocked}
|
||||
C->>C: blocked_user, Steam ID in user_steam_sync, display name in account_name
|
||||
A->>C: POST /internal/accounts/states {userId, steamId, displayName, blocked, roleChangedAt}
|
||||
C->>C: blocked_user, account_role_change, Steam ID in user_steam_sync, display name in account_name
|
||||
Note over A,C: nightly 04:15
|
||||
A->>C: PUT /internal/accounts/states (every user)
|
||||
```
|
||||
|
||||
Reference in new issue
Block a user