Authorization: permissions, roles and scopes
Signing in says who someone is. Authorization says what they may do: which routes they reach, which admin entities they see, and which records inside them. This guide covers declaring what can be granted, giving it to people, and moving an existing app onto enforcement. The reference is in the platform library (registry, scopes, route rules) and the admin library (entity access). Administrators managing access in a running app want Managing access.
The model in one paragraph
A permission is a keyword, <module>/<action>. A role bundles
permissions. A group holds roles, each with an optional scope that narrows
it to part of the data, such as one domain. A user holds the permissions of
every role of every group they are in, plus those of the built-in group
Everyone. A platform admin, a user with role :admin and no tenant, passes
every check.
Modes
:wagoe/authorization {:mode …} in config.edn decides how hard the checks
are:
|
Nothing is checked beyond sign-in. The framework default in 1.x, so an existing app is unaffected until it opts in. |
|
Nothing is refused. Each refusal |
|
Refusals hold: a missing permission is a 403, a record outside the scope a 404. |
wagoe new writes :mode :enforce into the dev and test profiles, so a new
app starts closed. In 2.0 :enforce becomes the default for every app.
Declaring permissions and roles
Every module has <module>/read and <module>/write without declaring them,
and the code roles <module>/viewer and <module>/editor holding them. A
module declares only what does not fit, in a def authorization in its wiring
namespace, or under :authorization in its ig-config:
(def authorization
{:permissions {:orders/refund {:label "Refund orders"}}
:roles {:orders/manager {:description "Everything in orders"
:permissions #{:orders/read :orders/write :orders/refund}}}})
bb scaffold writes this for a new module, with three code roles:
<module>/viewer, <module>/editor and <module>/manager. Code roles ship
with the code and are read-only in the UI. An administrator composes managed
roles from the registry at /web/admin/roles, or duplicates a code role there
to adapt it.
The boot fails when a code role or a route names a permission no module declares, so a typo is caught before a request is. A managed role holding one only logs a warning.
Giving routes a rule
Each route falls under the first rule that matches:
["/ping" {:public true :get {:handler ping}}] ; nothing checked
["/profile" {:authorization :signed-in :get {:handler me}}] ; any signed-in user
["/users/:id" {:authorization :self :get {:handler user}}] ; the handler checks
["/refunds" {:permission :orders/refund :post {:handler refund}}] ; that permission
["/orders" {:get {:handler list-all} :post {:handler create}}] ; orders/read, orders/write
The last falls back on the module the route came from: <module>/read for GET
and HEAD, <module>/write for the rest. A module’s contributed routes carry
their module, so a scaffolded module needs nothing more. A route with no rule
and no module, such as one mounted by hand outside a module, is refused to all
but a platform admin. A GET that writes needs an explicit :permission.
clojure -M:run authz-report prints every route with its rule and permission,
and the routes no rule covers, without starting the server; outside :off the boot logs the same list. An
app generated before the command existed can copy it from a new app’s
main.clj.
Scopes
A scope key names part of the data a right can be narrowed to:
{:scopes {:domain-id {:label "Domain" :entity :domains}}}
A route without :scope admits only holders whose right is unscoped.
:scope {:domain-id :path/domain-id} checks a path parameter;
:scope :handler leaves the filtering to the handler, through
scope/scope-filter. In the admin an entity maps a key to a field with
:scope {:domain-id :domain-id}, and lists, search and edits stay inside the
user’s scope. A value may also come from the user, so one group serves every
department. The platform library has the details.
Assigning rights
Administrators work in the user module’s pages, step by step in Access tasks:
-
/web/admin/groups: members, and roles with an optional scope. Everyone lists its roles, not its members. -
/web/admin/roles: code roles, locked, and managed roles as a checklist of the registry, grouped by module. -
/web/admin/permissions: every permission, and who holds it. -
A user’s Rights tab: their groups, and each permission with the group, role and scope it comes from. Add
:detail-tabstoadmin/users.ednto show it beside the edit page.
From code, wagoe.user.ports/add-group-role and import-groups! do the same;
see Authentication.
Give Everyone the roles every user should have. In the admin, each entity has
admin.<entity>/read and admin.<entity>/write, with the code roles
admin.<entity>/viewer and admin.<entity>/editor; a user needs read on one
entity to open /web/admin.
A new app
A new app runs :enforce and has no users. Create the first admin, who
passes every rule:
bb create-admin
Every other user signs in to a 403 on the module routes until an
administrator gives one of their groups a role, say product/viewer on
Everyone.
Moving an existing app to :enforce
bb upgrade authorization walks through these steps and names, per user,
what would change; see Upgrading an existing
app to permissions. By hand:
-
Upgrade and start once. The first start moves the old rights over, so nobody can do less under
:enforcethan under:off, and nobody becomes an admin: each group grant becomes a managed role, Everyone gets every module’seditorrole, and users with role:viewerjoin a group called Readers. -
Find the routes without a rule. Run
clojure -M:run authz-reportand give each route it lists a rule. -
Switch to
:reportand run the app as usual. Each lineAuthorization would refuse this request under :enforcenames the user, route and permission. Give the route a rule, or a group the role, until the lines stop. Under:reportthe admin still admits role:admin; its refusals are logged too. -
Check the admin. A tenant user with role
:adminis not a platform admin. Under:enforcethey need a group withadmin.<entity>/vieweror/editor.:require-rolein the admin config is read by nothing; delete it. -
Switch to
:enforce. Then narrow Everyone: replace theeditorroles the upgrade gave it with what every user should have.
If you use push notifications, the stats routes need push/read; give it to
whoever reads them before switching.