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
permissionstable. There is no schema change and no migration.
Rules worth knowing:
- Only users who hold
Role/updatecan open or save it. - You can't save a change to your own role that removes
Role/indexorRole/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.