Skip to content

Roles & permissions

Vantage ships a built-in screen for Django's auth.Group, treated as what every project uses it for: a role, a named bundle of permissions you grant to people. It lives at <panel>/_/roles/, appears under System in the sidebar and in the ⌘K palette, and needs no configuration, no model and no migration — it works on the tables django.contrib.auth already has.

The permission matrix for one role The permission matrix for one role

What it does

  • Roles list — every role with its member and permission counts. Create, duplicate (permissions are copied, members are not), rename and delete.
  • Permission matrix — one row per model, grouped by app; columns for the four standard verbs (view, add, change, delete) plus an Other column holding any custom permissions (Meta.permissions). Tri-state checkboxes toggle a whole row, a whole column, a whole app, or everything shown. Sections fold, and a filter box narrows rows by model name, app or codename.
  • Pending diff and review — the toolbar counts what you have changed (+12 / −3) and marks each changed cell. Review & save opens a dialog listing every grant and revoke; confirming sends one POST that is applied in one transaction, or not at all.
  • Members — add people with a searchable picker (the same combobox as every relation field) and remove them. Each member links to their effective permissions: the union of every role and their direct grants, with the source of each permission shown.
  • Delete preview — before a role is deleted, the page says how many people would lose access, counted net of every other role and direct grant they hold.

Models this panel does not register — framework tables, excluded models, content types whose model has been removed — are still listed, in a final Other models section, because those permissions are real and a role can hold them. The Hide models not in this panel toggle hides that section; the choice is remembered per browser.

Who can use it

Superusers only, by default. Widen it with ROLES_PERMISSION, which takes a permission string or a callable receiving the request — the same shape as SETTINGS_PERMISSION:

VANTAGE = {
    "ROLES_PERMISSION": "auth.change_group",
    # or: lambda request: request.user.groups.filter(name="IT").exists()
}

Getting in is not the same as being able to change things. Each write also asks the permission layer for the matching verb on auth.Group — add to create or duplicate, change to edit the matrix, rename or change members, delete to delete — so read-only mode turns the screen into a viewer, exactly as it does every other screen.

To switch the screen off for everyone, "ROLES_PERMISSION": lambda request: False: it leaves the sidebar and the palette, and every route answers 403.

Nobody can grant what they do not hold

Widening access is safe because of one rule, enforced on the server for every request: someone who is not a superuser can only grant or revoke permissions they hold themselves (as user.get_all_permissions() reports them, through every authentication backend). The cells they do not hold render locked, and a forged request that names them is ignored.

The same rule covers the indirect routes to escalation. Adding a member hands that person the role's whole bundle — so changing a role's members, renaming it, duplicating it or deleting it requires holding every permission the role grants. "Add myself to Admins" is refused rather than being a one-click escalation that never touched a checkbox.

The screen never shows or edits is_staff or is_superuser. Only a superuser viewing an account's effective permissions is told that the account is a superuser.

Safety properties

  • Every permission id is validated server-side. Ids are checked against the Permission table and the actor's reach; unknown, malformed or out-of-reach ids are dropped, and the save reports how many were ignored.
  • CSRF on every write — the views are wrapped in csrf_protect themselves, so they stay protected even in a project that removed CsrfViewMiddleware.
  • Conflicts, not clobbers. Each page carries a fingerprint of the role's name and permission set. The save compares it under a write lock on the group row; if a colleague changed the role in the meantime, nothing is saved and the page comes back (HTTP 409) showing what they granted and revoked, with your own changes re-applied on top for you to review and save again. Rename and delete are checked the same way. Membership changes are idempotent additions and removals, so they cannot clobber and carry no token.
  • Everything is audited. Each save writes one audit log entry on the role, naming every permission added and removed as app.codename; membership changes name the users added and removed; create, rename, duplicate and delete are recorded too.

Performance

The matrix is built from two queries — every Permission with its content type, and the role's permission ids — whatever the number of models. Grouping, verb detection and "is this model registered here" are worked out in Python against the app registry. The test suite asserts the page's query count is identical with and without three hundred extra models. Saving is a constant number of statements however many cells changed.

Keyboard and screen readers

The matrix is a table of native checkboxes, each named by its permission ("Can change order"), so Tab and Space work as everywhere else. Arrow keys move between cells like a spreadsheet (Home/End jump to the ends of a row), section headers are disclosure buttons with aria-expanded, and the tri-state headers expose the mixed state. Nothing is drag-only.

Without JavaScript the page still works: the matrix is a plain form, Review & save submits it directly, and the server works out the diff from the state the page was rendered with. (A role holding more than Django's DATA_UPLOAD_MAX_NUMBER_FIELDS permissions needs the script, which sends only the changes.)

Custom user models

Any AUTH_USER_MODEL works. Membership is found through the user model's many-to-many to Group (groups on PermissionsMixin); a user model without one has no members tab, and the matrix still manages the roles themselves. User search covers whichever of the username field, email field, first_name and last_name the model has.