Skip to content

Role and Permission Management

RBAC in kloudmint is table-driven: a Role has many Permission rows, each saying "this role may perform this action on this resource". Every RBACModelView checks this table on every request -- nothing is hardcoded.

1. Create a role

Roles are just rows in the Role table -- create them through the admin UI (Roles in the sidebar) or a seed script:

role = Role(name="viewer")
db.add(role)
db.commit()

2. Grant permissions to a role

A permission is one (role, resource, action) triple. resource is the model's class name ("Product", "User", ...); action is one of:

index    -- can see it in the list / sidebar
show     -- can view a record's details
create   -- can add a new record
update   -- can edit a record
destroy  -- can delete a record
db.add(Permission(role_id=role.id, resource="Product", action="index"))
db.add(Permission(role_id=role.id, resource="Product", action="show"))
db.commit()

A role with only index/show on Product and nothing else will see just "Products" in its sidebar, read-only -- no create/edit/delete buttons, and hitting those URLs directly returns 403 Forbidden.

3. Assign a role to a user

user.role_id = role.id
db.commit()

4. Managing permissions through the UI -- the dropdown auto-registry

Add PermissionFormMixin to your Permission admin view so resource and action render as dropdowns instead of free-text fields (a typo in free text silently grants nothing, with no error):

from kloudmint import RBACModelView, PermissionFormMixin
from models import Permission

class PermissionAdmin(PermissionFormMixin, RBACModelView, model=Permission):
    column_list = [Permission.id, Permission.role, Permission.resource, Permission.action]
    form_columns = [Permission.role, Permission.resource, Permission.action]

The resource dropdown is populated automatically from every RBACModelView you pass to Kloudmint.register(...) -- there's no list to maintain by hand. Register a new model tomorrow and it shows up in the dropdown the next time the app restarts.

To override this (e.g. restrict the dropdown to a subset), set resources explicitly on your subclass:

class PermissionAdmin(PermissionFormMixin, RBACModelView, model=Permission):
    resources = ["Product", "User"]   # only these show up, regardless of what's registered
    ...

5. Filtering the Permissions list page

With enough permissions rows, scrolling through pages of them gets tedious. PermissionFormMixin also adds Role/Resource/Action dropdown filters above the list table -- set role_model to get the Role filter (Resource/Action work regardless):

class PermissionAdmin(PermissionFormMixin, RBACModelView, model=Permission):
    role_model = Role   # the class create_rbac_models() returned
    column_list = [Permission.id, Permission.role, Permission.resource, Permission.action]
    form_columns = [Permission.role, Permission.resource, Permission.action]

These filters actually narrow the underlying query (not just visually hiding rows) -- e.g. filtering Resource to "Product" only returns Permission rows where resource = 'Product'. The Resource filter uses the same auto-registry as the form dropdown, so it stays in sync automatically too.

6. The role permission grid (checkbox UI)

Granting permissions row by row (section 4) gets tedious: a resource with five actions is five separate rows. Enable the grid instead and edit a whole role on one page -- resources down the side, actions across the top, a checkbox in each cell. (Added in 0.2.0.)

# main.py -- after Kloudmint(...) and admin.register(...)
admin.enable_permission_matrix(Role, Permission)
# admin_views.py -- adds a "Manage permissions" link beside each role's name
from kloudmint import RBACModelView, RolePermissionsLinkMixin

class RoleAdmin(RolePermissionsLinkMixin, RBACModelView, model=Role):
    column_list = [Role.id, Role.name]
    form_columns = [Role.name]

The grid lives at <base_url>/role-permissions/<role id> (e.g. /admin/role-permissions/3). It is not shown in the sidebar -- you reach it from a role's list or details page.

What it gives you:

  • a checkbox per resource and action, with a select-all box on every row and column
  • None / Read-only / Full access buttons that set the whole grid in one click
  • one Save that adds and removes rows in the existing permissions table. There is no schema change and no migration.

Rules worth knowing:

  • Only users who hold Role / update can open or save it.
  • You can't save a change to your own role that removes Role / index or Role / update -- that would lock you out of the page you need to undo it.
  • Rows the grid doesn't know about (a resource or action that isn't registered) are left untouched when you save, and values that aren't on the grid are ignored.
  • Every resource that has been registered shows up as a row, so custom pages (section 6a) appear too.

6a. Choosing which actions a resource offers

By default a resource shows the five CRUD actions. Narrow that for things that don't have all of them.

A model view that is read-only:

class AuditLogAdmin(RBACModelView, model=AuditLog):
    rbac_actions = ["index", "show"]

A custom page (a BaseView, which isn't registered automatically) that is only ever granted as use:

from kloudmint import register_resource

register_resource("Dashboard", actions=["use"])

The grid then shows one checkbox for Dashboard instead of five. Call register_resource("Name") without actions and the CRUD set is used. get_resource_actions("Name") returns whatever a resource has declared.

7. Protecting roles (e.g. super_admin) from edits and deletes

(Added in 0.3.0.) Make one or more roles impossible to change from the admin UI -- typically super_admin -- so nobody can rename it, strip its permissions or delete it by accident. Nothing is protected until you opt in, so upgrading doesn't change behaviour.

# main.py / admin setup
admin.protect_roles(Role, Permission)                          # protects "super_admin"
admin.protect_roles(Role, Permission, ["super_admin", "owner"])  # or name several
# admin_views.py -- put the mixins BEFORE RBACModelView so their checks run first
from kloudmint import ProtectedRolesMixin, ProtectedUsersMixin, RBACModelView, PermissionFormMixin

class RoleAdmin(ProtectedRolesMixin, RBACModelView, model=Role): ...
class PermissionAdmin(ProtectedRolesMixin, PermissionFormMixin, RBACModelView, model=Permission): ...
class UserAdmin(ProtectedUsersMixin, RBACModelView, model=User): ...

For a protected role:

  • the role can't be edited or deleted (buttons disappear, and the URLs return 403, including bulk delete -- one protected row in a selection refuses the whole request)
  • its permission rows can't be edited or deleted, and none can be added
  • the permission grid (section 6) shows it read-only, and saving is refused

Users stay fully editable. ProtectedUsersMixin only adds two guards:

  • you can't delete your own account
  • you can't delete, deactivate, or move off the role the last active user who holds a protected role

If your User (or Role/Permission) view defines its own on_model_change, call await super().on_model_change(data, model, is_created, request) from it -- otherwise the guard in the mixin never runs. Role names are looked up by name and cached for about 30 seconds, so renaming a protected role in the database directly takes up to that long to be noticed.

8. How the check actually runs

Every RBACModelView action is gated by has_permission(request, resource, action) (src/kloudmint/rbac.py), which reads request.state.admin_user.role.permissions (set by your KloudmintAuth.load_user) and looks for a matching (resource, action) pair. No match -> denied.

sqladmin hook permission checked
is_accessible / is_visible (sidebar + view access) index
check_can_view_details show
check_can_create create
check_can_edit update
check_can_delete destroy

See KnownLimitations.md for the one action (export) that can't be gated this way.