Plugin Authorization
Plugin authorization is derived entirely from your routes. You attach an authorization rule to each HTTP route handler — referencing typed permissions and declaring the route’s OAuth scope — and the platform reads those rules back to build the permission catalog, the per-endpoint bindings, and the plugin’s namespace when it compiles the OPA bundle. There is no separate permission list to keep in sync with your routes.
This page is for plugin authors. For the platform-wide RBAC model see Roles & Permissions; for how decisions are evaluated see Policy Engine; for token-level scopes see API Scopes.
How Plugin Authz Is Derived
Every plugin HTTP route must carry an authorization rule — a @path_rule (attached directly, or via a route factory’s authz=). A route with no rule is a validation error. Under the default fail-mode the auth service refuses to build the OPA bundle rather than silently fencing (or exposing) the route, so a missing rule fails the whole platform closed at startup instead of leaking at request time.
The rule references Permission objects; the platform derives the normalized policy from the routes when the bundle is built:
- the permission catalog (each permission’s id and description),
- the per-endpoint bindings (method + path → permissions, scope, caller kinds),
- the plugin namespace.
The permission is the object referenced on the route, and it carries its own description — so there is nothing to declare twice. The only authz a plugin declares apart from its routes is the optional escape hatch for permissions that have no 1:1 route (see Permissions Without a Route).
The Authorization Model
Three orthogonal pieces ride on a route: the permissions it requires, the OAuth scope it sits behind, and the caller kinds allowed to satisfy it.
Permissions — the service.resource.action grammar
A permission id follows the grammar <service>.<resource>.<action>, joined from its parts:
service— the owning plugin. It must equal yourNemoService.name; this namespace fence is enforced fail-closed (an out-of-namespace permission fails the whole plugin).resource— the collection or sub-resource acted on. May be empty for a service-level action, and may itself be dotted.action— the operation (create/list/read/update/delete, …).
You never hand-build id strings. Declare permissions in a typed PermissionSet and reference the members:
Inside a PermissionSet(namespace="a.b"), perm("desc") mints the id a.b.<member-name-lowercased>. For a compound id, pass suffix=:
Referencing a PermissionSet member that doesn’t exist is an AttributeError at import, and passing a bare string where a Permission is expected is a TypeError at decoration — a permission typo can never reach the policy layer.
OAuth scopes
A plugin owns one AuthzScope, which mints both the coarse OAuth scope and the permission namespace beneath it. AuthzScope("myplugin") gives the route decorators @scope.read and @scope.write, which stamp the scope requirement myplugin:read / myplugin:write (plus the catch-all platform:read / platform:write). A route carries exactly one scope.
When the permission namespace nests deeper than the OAuth scope, use child() — the scope stays put while the permission id deepens:
Caller kinds — PRINCIPAL and SERVICE_PRINCIPAL
CallerKind is a subject attribute enforced in Rego, not a permission. A route is intended for a normal authenticated user (CallerKind.PRINCIPAL) or a caller whose id is prefixed service: — e.g. a controller writing observed status (CallerKind.SERVICE_PRINCIPAL). There is intentionally no anonymous kind; the only genuinely public routes are core infrastructure, hardcoded as a bypass in the enforcement point.
Declaring Authz on a Route
For a hand-written route, @router.<method> is the outermost (top) decorator; @scope.read/@scope.write and @path_rule sit under it. The scope decorator and the path rule are independent, so their order relative to each other does not matter.
@path_rule takes:
callers— a required, non-empty list of caller kinds. They are OR’d.permissions—Permissionobjects the caller must hold. They are AND’d, and may be[]for an authenticated-but-permissionless route (e.g. a health check or a raw blobPUT).
Stacking @path_rule on the same handler adds OR-alternatives: any satisfied rule allows the request.
A controller-only write route pins the caller kind to SERVICE_PRINCIPAL and requires the compound permission:
An authenticated-but-permissionless route passes permissions=[]:
Job and Function Routes
Jobs and functions are mounted through route factories rather than hand-written handlers. You must pass authz= — the kwarg defaults to None, and a factory called without it leaves its routes unruled, which fails closed at bundle time.
When authz= is set, add_function_routes stamps a PRINCIPAL rule on the route and mints an invoke permission from the scope (<namespace>.<function-name>, a write action). permission_description is optional prose for that permission and requires authz — supplying it alone is a ValueError.
Permissions Without a Route
Some permissions are not 1:1 with a route — they are checked in middleware, declared ahead of the route that will reference them, or need to be granted to a role the default heuristic misses. Declare these on your NemoService via two escape hatches; both are merged into the derived catalog.
The derivation grants each catalog permission to roles by a suffix heuristic:
Use extra_role_permissions when that heuristic is wrong — e.g. an agent gateway’s .invoke permission that a Viewer must hold to call a deployed agent even though its suffix isn’t read. Grants are unioned with the suffix defaults (never subtractive), every granted permission must live in this service’s own namespace (or the whole plugin fails closed), and a permission granted here is registered in the catalog automatically, so it need not also appear in extra_permissions.
When a Plugin’s Authz Is Invalid
An unruled route, or a rule referencing an undeclared or out-of-namespace permission, makes a plugin’s authz invalid. The offending routes are always emitted as explicit denies; the auth service’s on_invalid_plugin fail-mode (env prefix NMP_AUTH_) controls the blast radius:
The default hard_fail matches the rule that a missing path rule is a validation error. A deployment that loads dynamically-discovered or third-party plugins CI never vetted can downgrade to quarantine or deny_route so one bad plugin can’t wedge the platform.
Verifying Your Plugin’s Authz
From the repo root, with the workspace installed, check that every route is ruled:
Then rebuild and test the compiled policy:
The compiled policy.wasm in the auth service is a gitignored build artifact. Run make build-policy after pulling authz or Rego changes — otherwise the embedded-PDP tests run against a stale copy and fail with misleading errors.
Related
- Roles & Permissions — Predefined roles and how permissions map to them.
- Permissions Reference — The full catalog of derived permissions across every API.
- Policy Engine — How the PDP evaluates permissions, scopes, and role bindings.
- API Scopes — Token-level scope restrictions layered on top of role permissions.