A single, point-and-click console to manage every user's access across Campus+ — assign roles, grant or revoke individual pages, and audit every change. Built on the existing custom RBAC without touching roles, the sidebar engine, or the mobile app. Every screenshot below is the live local Docker build running on real production data.
A user's effective access is resolved by a single source of truth shared by the page gate, the sidebar, and this module — so they can never disagree. Precedence, highest first:
Assign a whole role like Venue Admin in one click — the user inherits all its pages and the role reaches the mobile app, which gates features by role name.
Give one user a single extra page without assigning a role — stored on the user, affecting nobody else.
Remove one page from one user even when a role grants it — the role stays intact for everyone else.
Every grant, revoke, role change and undo is written to an immutable trail with actor, time and reason.
| Concept | Where it lives | Affects the mobile app? |
|---|---|---|
| Role membership | UserRecord.user_groups | Yes — app reads role names |
| Direct grant | UserRecord.permissions | No — web only |
| Revoke | UserPermissionRevoke (new) | No — web only |
| Audit | AccessAuditLog (new) | — |
The entry point — a searchable directory of every user with their role chips, status and a one-click Manage access. Search by name, email or roll number; filter by active / blocked.

manage-access permission.One page to manage a user's entire access. Three zones: Roles (tiles you click to grant a whole role), Add a specific permission (search instead of scrolling), and What this user can access (a clean module→page tree with one-click remove). The live "can access" counter and a Recent-changes feed sit at the top-right.

Clicking a role never fires blindly — it opens a popup asking how much to grant. Grant whole role assigns the actual role (and clears any stale revokes on its pages, so they all switch on). Choose specific… lets you pick individual pages instead, granted to just this user without assigning the role. An assigned role instead offers Remove this role and, if some pages are off, Turn on all pages.

Expands the role into its individual pages, each a toggle reflecting the user's current state. Flip only the ones you want — they're granted directly to this user, leaving the role itself untouched for everyone else.

Rather than scroll 194 permissions, type a few letters. The search instantly filters every page across all modules and shows a toggle for each — flip it on and the page is granted live, appearing in the access tree below.

A clean tree grouped by module. Each module folds; under it every page is listed with its name, codename and a source badge — role (from a role), direct (granted to this user), revoked. One-click Remove on any page.

An immutable, filterable trail of every access change — grants, revokes, role assigns/removes and undos — each with the actor, timestamp, the permission/role, and a reason. Filter by action type and paginate.

UserPermissionRevoke and AccessAuditLog. One additive migration, zero changes to existing tables.base_app/access.py resolves effective access (superuser > revoke > grant) and is used by the page gate, the sidebar and this module, so they never drift.manage-access; the module is gated by it (superusers bypass to grant the first admin).permission_objects.py sync, the legacy type field and the mobile API are all untouched. The only edits to existing files add a revoke-aware check that is a no-op until a revoke exists.