Policies and roles
On this page:
- spider.rbac: The checks behind a route's
.roles,.org_roles,.authenticatedand.policy, as middlewares. - spider.policy: A named access rule for a route:
.policy = spider.policy("post_owner", isPostOwner)withfn isPostOwner(c: *spider.Ctx) !bool. See modules/rbac.zig. - spider.Policy: What
spider.policyandspider.resourcePolicyreturn: the value of a route's.policy. - spider.resourcePolicy: A policy that loads the route's resource (404 when missing), checks it, and hands it to the handler (spider.Loaded(T) / c.loaded(T)).
- spider.policySet: The rules about one kind of resource in one place (Laravel Policy / Pundit):
Set.route(.update)for routes,Set.can(c, .update, x)in handlers.
spider.rbac
Access rules of a route (spider.rbac): the middlewares behind the route config keys .authenticated, .roles, .org_roles and .policy, and the builders spider.policy, spider.resourcePolicy and spider.policySet. They read the identity an auth middleware put on the request.
spider.rbac.routeMiddlewares
pub fn routeMiddlewares(comptime config: anytype) []const MiddlewareFn
RBAC middlewares for a route config (.{ .authenticated = ..., .roles = ..., .org_roles = ..., .policy = ... }), resolved at comptime. Every field present must pass: each becomes its own middleware, in that order (the policy last). An empty .roles or .org_roles list adds none.
spider.rbac.requireAuthenticated
pub fn requireAuthenticated(c: *Ctx, next: NextFn) anyerror!Response
.authenticated = true: any logged-in user; error.Unauthorized (401) when the request carries no identity (c.userId() is null: no _auth_sub from spider.session, jwks, keycloak, clerk or c.setUser, and no _user_id from the HS256 auth middleware) — also when the app has no auth middleware.
spider.rbac.requireRoles
pub fn requireRoles(comptime roles: []const []const u8) MiddlewareFn
Returns a middleware that requires the user to hold at least one of roles (roles of the account: c.hasRole); otherwise error.Forbidden (403), or error.Unauthorized (401) for a request that is nobody's: no user, no role and no organization. Must run AFTER the auth middleware.
spider.rbac.requireOrgRoles
pub fn requireOrgRoles(comptime roles: []const []const u8) MiddlewareFn
Returns a middleware that requires the user to hold at least one of roles in an organization (the org roles the auth provider read from the token, or c.addOrgRole); otherwise error.Forbidden (403), or error.Unauthorized (401) for a request that is nobody's.
When an active org is set (c.activeOrgId(), from the provider's active_org_cookie or an app middleware calling c.setActiveOrg), only roles held in THAT org count — being admin of org A does not open a route while org B is active. With no active org selected, a role held in any of the user's orgs is accepted.
spider.rbac.Policy
pub const Policy = struct
A named access rule for rules that aren't a role: "owns the post", "the org is on a paid plan", "the webhook signature is valid". Declared on a route as .policy = spider.policy("post_owner", isPostOwner); the name is what the route listing, routes.lock and expectRoutes show.
Fields
name: []const u8: What the route listing shows for the rule.check: *const fn (*Ctx) anyerror!bool: true lets the request through. Build a Policy withspider.policy, which wraps aboolor!boolfunction into this shape.
spider.rbac.policy
pub fn policy(comptime name: []const u8, comptime check: anytype) Policy
check is fn (*Ctx) bool or fn (*Ctx) !bool, and runs after the route's .authenticated / .roles / .org_roles checks. When it says no, the request fails with 403, or 401 when there is no logged-in user (like ASP.NET's Forbid / Challenge); an error it returns is the request's error. name: letters, digits and _ . : -.
spider.rbac.requirePolicy
pub fn requirePolicy(comptime p: Policy) MiddlewareFn
The middleware behind a route's .policy.
spider.rbac.Deny
pub const Deny = enum
What a resourcePolicy answers when its check says no.
Values
forbidden: 403: the caller learns the resource exists.not_found: 404, like a missing one: ids can't be probed (GitHub's approach for private repositories).
spider.rbac.resourcePolicy
pub fn resourcePolicy(comptime name: []const u8, comptime T: type, comptime opts: anytype) Policy
A policy about the one resource a route works on (a post, a ticket):
.policy = spider.resourcePolicy("post_owner", Post, .{
.load = loadPost, // fn (*Ctx) ?Post, !?Post or !Post — from c.params etc.
.check = isOwner, // fn (*Ctx, *const Post) bool or !bool
.deny = .forbidden, // or .not_found (optional)
})
In order: an anonymous request on a non-public route is refused (401) before anything is loaded; a missing resource is error.NotFound (404); then check decides (403, or 404 with .deny = .not_found; a refused anonymous request on a public route gets 401, not 403). An allowed resource reaches the handler, loaded once, as spider.Loaded(Post) or c.loaded(Post). SSE handlers (*Sse) don't see it.
spider.rbac.policySet
pub fn policySet(comptime T: type, comptime opts: anytype) type
The rules about one kind of resource, in one place — like a Laravel Policy class or a Pundit policy:
pub const Tickets = spider.policySet(Ticket, .{
.name = "ticket", // policies listed as ticket.view, ticket.update...
.load = service.loadTicket, // as in resourcePolicy
.deny = .not_found, // default for every rule (optional)
.rules = .{
.view = canView, // fn (*Ctx, *const Ticket) bool or !bool
.update = isOwner,
.delete = .{ .check = isAdmin, .deny = .forbidden }, // per-rule deny
},
});
Routes take .policy = Tickets.route(.update) (a resourcePolicy: 401 / 404 / 403 as documented there, the ticket reaches the handler as spider.Loaded(Ticket)); a handler asks try Tickets.can(c, .update, ticket) (e.g. to show an edit button). An action missing from .rules doesn't compile.
rbac.policySet.Resource
pub const Resource = T
The type the set is about (T).
rbac.policySet.name
pub const name: []const u8 = opts.name
The set's .name: the prefix of its policies' names ("ticket" in "ticket.update").
rbac.policySet.route
pub fn route(comptime action: @TypeOf(.enum_literal)) Policy
The policy for a route: .policy = Tickets.route(.update).
rbac.policySet.can
pub fn can(c: *Ctx, comptime action: @TypeOf(.enum_literal), value: *const T) !bool
Whether the request's user may do action to value (already loaded). Calls the rule as is: anonymous users are whatever the rule makes of them.
rbac.policySet.find
pub fn find(c: *Ctx) !?T
Runs the set's .load for this request and returns what it found (null when there is none), with no rule checked and nothing handed to c.loaded. Its error is the loader's.
spider.policy
The same as spider.rbac.policy.
spider.Policy
The same as spider.rbac.Policy.
spider.resourcePolicy
The same as spider.rbac.resourcePolicy.
spider.policySet
The same as spider.rbac.policySet.