Permissions
Principal, resource, action: permit tables per service, PermissionsGate on every mounted router, observe-only for migration, and the same engine for Action List runs.
What it is and when to use it#
The model is a Principal (a user, a guest, a service, a host device, or
anonymous) doing an Action (read, write, comment, admin, download, run) on
a Resource (a path such as org/fts/files/**), and a PermissionEngine
answering Allow or Deny. Each service has a ServicePermits table naming,
per method, the action and resource it needs. PermissionsGate checks every
vox call against those tables before the method runs, and denies what it
cannot place: a method a table leaves out, or a service with no table
(unless told otherwise). observe_only audits what it would deny and lets it
through, for turning the gate on in a running app.
Mount every service behind the gate and give the Action List the same engine
(set_authorizer): the router gate does not cover in-process action runs.
There is no architecture page for the gate; the reference is
skills/architect/references/auth-permissions.md. Signed, expiring grants
are a different tool: Capability tokens.
A minimal example#
architect-permissions; the gate is architect::permissions_gate (feature
vox, native). From features/auth/tests/native/src/permissions_gate_tests.rs:
use architect::permissions_gate::{PermissionsGate, UnlistedPolicy};
use architect_permissions::{MethodPermit, RoleEngine, ServicePermits};
use auth::identity::SessionIdentityResolver;
const PROBE_PERMITS: ServicePermits = ServicePermits {
service: "gate-probe",
methods: &[
MethodPermit::new("read_thing", "read", "probe/**"),
MethodPermit::new("write_thing", "write", "probe/**"),
],
};
let mut roles = RoleEngine::new();
roles.set_member(alice.user.id.to_string(), "member");
let gate = PermissionsGate::new(Arc::new(roles), Arc::new(SessionIdentityResolver::new(auth.clone())))
.unlisted(UnlistedPolicy::Deny) // the default
.observe_only(!enforce)
.permit(gate_probe_service_descriptor(), &PROBE_PERMITS);
assert!(gate.coverage(&router).is_complete());
let gated = router.with_permissions(gate);
On a server, hand the gate to the host instead:
EngineHost::new(router, gate, bind) puts it in front of /vox, the iroh
lane and every vox_at lane (EngineHost).
By hand, per connection, with the connection’s bearer:
PermissionsGate::wrap_shared_with_bearer(Arc::new(gate), router, bearer);
for a peer whose endpoint the transport proved,
PermissionsGate::wrap_shared_with_proved_caller(gate, router, Principal::Host { endpoint }).
Inside a handler, architect::permissions_gate::caller() is the principal
the gate resolved. For actions: list.set_authorizer(engine) and run with
run_json_as or run_json_async_as.
Code tour#
features/permissions/src/lib.rsPrincipal,Resource,Action,Decision: the model.features/permissions/src/lib.rsPermissionEngine(check,authorize) andIdentityResolver: the two traits a gate needs.features/permissions/src/lib.rsRoleEngine,ScopeEngine,CompositeEngine,AllowAll,Rule: engines to start from.features/permissions/src/lib.rsMethodPermit,ServicePermits,AuditSink,TracingAudit: the tables and the audit trail.features/auth/auth/src/identity.rsSessionIdentityResolver: a bearer to a user or anonymous.crates/architect/src/permissions_gate.rsPermissionsGate(permit,unlisted,observe_only,coverage,wrap_shared_with_bearer,wrap_shared_with_proved_caller),PermissionedRouter,caller,GateCoverage.crates/architect/src/action_list.rsActionList::set_authorizer,run_json_as,infos_for,ActionCaller: the same engine for actions.features/permissions/proto/src/lib.rsPermissionsService: an advisory “can I?” service a UI asks before showing a control.
A call reaches PermissionedRouter, which takes the bearer from the call’s
metadata or else the connection’s. It maps the method id to its permit,
resolves the principal once, and asks the engine about the permit’s resource
({arg} placeholders widen to **). On Allow the method runs with the
principal set, so caller() sees it. On Deny the reply is permission denied in the method’s own response shape, unless observe_only is on, in
which case the denial is audited and the call goes through.
Tests to read#
features/auth/tests/native/src/permissions_gate_tests.rs: roles enforced over validated sessions; a scope engine gating by prefix; observe-only letting denials through; a handler seeing the resolved caller; an untabled service denied by default and served when allowed; coverage naming every gap.- Unit tests in
crates/architect/src/action_list.rs: the authorizer decides who runs what; a caller sees only what it may run. features/scripting/tests/scripts.rs: a script runs with its caller’s permissions.features/orgs/tests/server.rs: the gate with an org-confined engine.
Status#
Done in architect. Limits: the gate checks the method’s coarse resource,
not each argument; a malformed token is treated as anonymous; revocation lags
by the resolver’s cache (30 s); the advisory service checks as anonymous;
the gate and the Action List are two planes until the app wires
set_authorizer.
- Task gates its server (
task:apps/server/src/lib.rs, tables intask:apps/server/src/permits.rs) with unlisted services allowed and enforcement behindTASK_ENFORCE_PERMISSIONS=1. Still to do: deny unlisted, enforce by default,set_authorizeron its Action List. - Studio depends on
architect-permissionsbut mounts no gate yet.
Linked from
- FeaturesOne page per major architect feature: what it is, the least code to use it, a tour of its code, the tests to read, and its status.
- EngineHost and protocol mountsOne server builder: /health, /vox and /version.json always; plugins, protocol mounts and iroh by builder method; the permissions gate on every vox lane.
- AuthAccounts and sessions: the auth engine, its services on every transport, the client kit (token store, OIDC with PKCE, device flow), and apps/auth-server.