feat(19.6.1-01): administrator form with server-side privilege rules
- create and edit administrators: password (min 4, max 72 bytes, bcrypt at admin.password.bcrypt_cost), role picker, radio permission overrides, superuser switch; no mail on create - superusers hidden from non-superusers in list, lookups and schemas; is_superuser projected to superusers so an untouched save keeps it - save-path rules: superuser-only switch, role and permission ceilings, no outranking, no own role/flag/overrides/password change, last active superuser not demoted (advisory lock + count in the write tx) - password set for another admin bumps tokens_valid_after - role, superuser, activation and last sign-in list filters
This commit is contained in:
@@ -68,6 +68,8 @@ func (p *BlogPlugin) Navigation() []pact.NavigationItem {
|
||||
|
||||
A controller's `pact.AdminPermissioned.RequiredPermissions` are checked before any schema is served or query runs, and navigation and settings entries are filtered by the permissions they name, so an administrator sees only what they may open: a main menu item the administrator may not open is dropped even when one of its side-menu entries would pass, and an allowed item that links to a controller the administrator cannot open links to its first openable side-menu entry instead. A plugin can also hide an entry per request with `pact.NavigationItem.Visible` (for a screen whose feature is turned off in its settings); the same drop and relink rules apply. `cabana.Allows` is the check and follows Winter's `hasAnyAccess` for JWT sessions: superusers pass, an administrator needs any one of the listed codes, and an empty requirement list allows any signed-in administrator. Wildcards match on both sides: a grant ending in `.*` covers every code with that prefix, and a required code such as `acme.blog.*` is met by any grant under `acme.blog.`. When the request carries a personal API token with a non-full scope, the scope is checked first (see [API tokens](#api-tokens)). The last lines of the activation example on [Admin controllers](admin-controllers.md) show it.
|
||||
|
||||
The admin registers two permissions of its own on the System tab of the permission editor: `backend.manage_users` (`cabana.PermissionManageUsers`) opens the Administrators screen and `backend.manage_roles` (`cabana.PermissionManageRoles`) the Roles screen. Neither is granted to any role in code, so after an upgrade only superusers reach those screens until an administrator grants a code to a role or to an administrator.
|
||||
|
||||
An administrator's grants are the role's `permissions` merged with the administrator's own `backend_users.permissions`, the way WinterCMS merges them: the administrator's value for a code replaces the role's, and only `1` grants. A `-1` (or `0`) on the administrator therefore removes a permission the role grants, so rows copied from a WinterCMS database keep their denies. As in WinterCMS the merge compares codes exactly, so denying `acme.blog.access_posts` does not take it back from a role that grants `acme.blog.*`.
|
||||
|
||||
Actions registered through `pact.HasAdminActions`, bulk actions (`pact.HasAdminBulkActions`) and record actions (`pact.HasAdminRecordActions`) may each name extra permissions, checked on top of the controller's. An administrator who lacks them does not get the action in the list schema or in a record's `meta.actions`, and posting it answers 403 and is written to the authentication log. A bulk or record action that ran is logged with the controller, the action, the administrator and the affected count or record id, without record contents.
|
||||
@@ -135,7 +137,7 @@ Operators manage any user's tokens with `admin:token:create`, `admin:token:list`
|
||||
|
||||
Every signed-in administrator can edit their own profile on the My account screen, which the SPA opens at `<prefix>/account` from the user menu. It saves through `PUT <prefix>/api/v1/account`, the Go form of WinterCMS's `Users::myaccount`. The route always edits the row of the token's administrator; no id is read from the path or the body. Personal API tokens are refused on this route and on the avatar routes.
|
||||
|
||||
- **Editable:** first name, last name, email and subtitle, plus the password. The body may hold only `first_name`, `last_name`, `email`, `subtitle`, `current_password`, `password` and `password_confirmation` (`cabana.AdminAccountRequest`). Any other key, such as `login`, `role_id`, `is_superuser`, `permissions` or `is_activated`, answers 422 on that key and nothing is written. The login is shown but cannot be changed here, and role, superuser flag, permissions and activation stay with the operator commands below.
|
||||
- **Editable:** first name, last name, email and subtitle, plus the password. The body may hold only `first_name`, `last_name`, `email`, `subtitle`, `current_password`, `password` and `password_confirmation` (`cabana.AdminAccountRequest`). Any other key, such as `login`, `role_id`, `is_superuser`, `permissions` or `is_activated`, answers 422 on that key and nothing is written. The login is shown but cannot be changed here. Role, superuser flag, permissions and activation are changed by another administrator on the Administrators screen, or with the operator commands below.
|
||||
- **Email:** required, a valid address and 6 to 255 characters long. It must not equal, ignoring case, the email or login of another administrator, deleted administrators included, because a sign-in identifier that matches two administrators locks both out. Your own email in a different case is accepted. Changing the email does not ask for the password, as in WinterCMS: the framework sends no password-reset emails, so the email is not a recovery channel.
|
||||
- **Password:** leave it blank to keep the current one. A new password needs `current_password`, at least 4 characters, at most 72 bytes (bcrypt's limit) and an equal `password_confirmation`. A wrong current password answers 422 on `current_password` and is written to the authentication log. A changed password is hashed at `admin.password.bcrypt_cost` and ends every other session: tokens issued before the change are refused. The browser that made the change gets a fresh session cookie and stays signed in; an API client that used a Bearer token has to sign in again.
|
||||
- **Subtitle:** a short line such as a role or position. It is stored where WinterCMS keeps it, under the `subtitle` key of the jsonable `backend_users.metadata` column, so rows copied from WinterCMS keep their subtitles and other metadata keys survive a save. Code reads it with `cabana.BackendUser.Subtitle`, which treats NULL, `[]` and malformed metadata as no subtitle. `GET <prefix>/api/v1/auth/me` returns it as `subtitle`.
|
||||
@@ -147,6 +149,25 @@ Plugins that show an administrator, such as an author box, read the subtitle wit
|
||||
|
||||
## Managing administrators
|
||||
|
||||
### The Administrators screen
|
||||
|
||||
Settings → System → Administrators (`summercms.cabana.users`, `cabana.AdministratorsControllerID`) lists the backend administrators and opens a preview of each; it needs `backend.manage_users`. The list searches login, names and email and filters by role, superuser access, activation and last sign-in. The form has two tabs:
|
||||
|
||||
- **Account:** login, email, first and last name, the password with its confirmation, and the role. A password is required when an administrator is created and optional when one is edited (blank keeps the current one); it needs at least 4 characters, at most 72 bytes (bcrypt's limit) and an equal confirmation, and is hashed at `admin.password.bcrypt_cost`. A login or email another administrator already uses, in either field and ignoring case, is refused on the field; when only a deleted administrator holds it, the message says so, so you can restore that account instead. A new administrator is created activated, like `admin:create`, and no mail is sent.
|
||||
- **Permissions:** the superuser switch and the per-administrator permission overrides (inherit, allow, deny for each code in the catalog), stored in `backend_users.permissions` and applied last over the role's grants.
|
||||
|
||||
The server enforces these rules on every save, whatever the client sends:
|
||||
|
||||
- Only an administrator who is a superuser (signed in, or with a personal API token of full scope) sees superusers at all: for everyone else they are left out of the list, their ids answer 404, and the superuser switch, column and filter are not in the schemas. Sending `is_superuser` without being a superuser is refused with 403, even when the field is hidden.
|
||||
- An administrator cannot grant more than they hold. Permissions they lack are locked in the override editor (a changed locked value is refused with 403), and roles whose grants (stored and code-declared) they do not hold themselves are locked in the role picker: assigning one is refused, while an administrator who already has such a role keeps it when other fields change.
|
||||
- An administrator who is not a superuser cannot change, deactivate or delete an administrator whose grants they do not all hold themselves.
|
||||
- On their own record an administrator may change login, email and names. A change of their own role, superuser flag or permission overrides is refused with 403, and so is a password: an administrator changes their own password in My account, which checks the current one.
|
||||
- A superuser cannot be demoted when it is the last active superuser.
|
||||
|
||||
Setting the password of another administrator ends that administrator's sessions at once, as `admin:reset-password` does: tokens issued before the change are refused. Their personal API tokens keep working, exactly as after `admin:reset-password`; revoke them with `admin:token:revoke` when they should stop too.
|
||||
|
||||
### Operator commands
|
||||
|
||||
The application binary has commands for operators:
|
||||
|
||||
```sh
|
||||
|
||||
Reference in New Issue
Block a user