# Access Control and Users

## Phase 3 scope

Phase 3 owns the staff account lifecycle, relational roles, deployment-defined
permissions, authorization policies, privileged-access controls, concurrency
rules, and the append-only audit primitive used by these operations.

Permissions are canonical application capabilities declared by
`PermissionName` and synchronized by `AccessControlSeeder`. Administrators do
not create, rename, or delete permission definitions at runtime. They create
custom roles and select from the catalog. The protected `super-admin` system
role remains deployment-controlled and grants all permissions.

## Enforced invariants

- Every management endpoint requires authentication, verified email, any
  required temporary-password change, privileged 2FA, and recent password
  confirmation before its Policy and Form Request run.
- User creation requires both `users.manage` and `roles.manage`; role creation
  requires both `roles.manage` and `permissions.manage`.
- Role assignment requires an active role. An inactive role stops granting
  permissions immediately and cannot be newly assigned.
- The last active super administrator cannot be deactivated, deleted, or have
  the protected role removed.
- System roles cannot be edited, deactivated, have permissions replaced, or be
  deleted through runtime management.
- Assigned custom roles cannot be deleted. Membership must be removed first.
- Administrators cannot deactivate their own account through the web workflow.
- User and role mutations compare `lock_version`; stale forms are rejected
  rather than overwriting newer state.
- Deactivation revokes sessions, rotates persistent credentials, and deletes
  outstanding password-reset tokens in the same transaction.
- Reactivation forces a new password and queues fresh onboarding instructions.
- Changing a sign-in email clears verification, rotates the remember token,
  revokes sessions, invalidates old-email recovery tokens, and queues new
  verification/password setup notifications.

## Transaction and locking order

Operations that can affect the protected invariant lock the `super-admin` role
first. Requested roles are then locked in ascending primary-key order, followed
by the target user. This stable ordering avoids role/user lock inversion.
Changes and their audit rows commit atomically. MySQL deadlock retries are
bounded to three attempts.

The super-admin role row acts as the serialization point for changes that could
remove an active protected member. The final invariant query locks any other
active protected member before allowing the change. `lock_version` independently
protects browser edits from lost updates.

## Account lifecycle

### Create

The server normalizes and uniquely validates email, creates an active unverified
account with a cryptographically random unusable initial password, sets
`force_password_change`, attaches validated active roles, and writes a redacted
audit snapshot. Verification and password setup notifications are queued after
commit.

### Update

Profile data is updated under a user row lock after an optimistic-version check.
Audit snapshots contain an HMAC of email instead of the address. Role replacement
is a separate authorized operation so profile authority cannot be used to gain
roles.

### Deactivate/reactivate

Deletion is intentionally absent from the administration UI. Deactivation is
the normal access-removal operation and preserves history. Reactivation starts a
fresh credential-verification cycle. The lower-level deletion guard remains for
future privacy/retention workflows and preserves the protected-role invariant.

## Authorization map

| Operation | Required capabilities |
|---|---|
| View/update user profile or status | `users.manage` |
| Create user | `users.manage` + `roles.manage` |
| Replace user roles | `users.manage` + `roles.manage` |
| View/update/delete custom role | `roles.manage` |
| Create role | `roles.manage` + `permissions.manage` |
| Replace role permissions | `roles.manage` + `permissions.manage` |

`users.manage`, `roles.manage`, `permissions.manage`, `settings.manage`, and
`integrations.manage` are classified as privileged. Accounts receiving any of
them must confirm 2FA before access.

## Audit contract

`audit_logs` records actor, action, stable subject identifier, request ID, IP,
redacted before/after JSON, result, and occurrence time. The Eloquent model
rejects update and delete operations. Production database credentials should
also deny `UPDATE` and `DELETE` on this table to protect against query-builder or
manual mutation outside the model.

Passwords, tokens, remember credentials, TOTP secrets, and recovery codes are
recursively redacted. User email is represented by the configured audit HMAC,
not plaintext. Ordinary account deletion nulls the actor FK but leaves the
subject identifier and event evidence intact.

## Query and cache behavior

- User lists paginate at 20 rows and eager-load roles.
- Role lists use database counts for users and permissions.
- Pivot primary keys prevent duplicate membership and permission grants.
- Permission and privileged-2FA checks use indexed relational `exists` queries.
- Authorization results are deliberately not cached across requests so role
  deactivation and permission removal take effect immediately.

## Verification evidence

Automated coverage includes policy denial, dual-capability separation, user
creation/onboarding, normalized unique email, optimistic locking, role
replacement, last-super-admin protection, self-deactivation denial, session
revocation, email reverification, activation lifecycle, role creation and
deactivation, system-role immutability, assigned-role deletion prevention,
privileged 2FA, redacted audit snapshots, and append-only model behavior.

The complete SQLite test suite is the local fast gate. The existing CI workflow
runs the same migrations and suite against MySQL 8.4, which is the certification
gate for InnoDB locking and SQL behavior. That remote MySQL job has not been run
from this local environment because Docker/MySQL 8 is unavailable here.
