Zig 0.17.0

Policies and roles

On this page:

  • spider.rbac: The checks behind a route's .roles, .org_roles, .authenticated and .policy, as middlewares.
  • spider.policy: A named access rule for a route: .policy = spider.policy("post_owner", isPostOwner) with fn isPostOwner(c: *spider.Ctx) !bool. See modules/rbac.zig.
  • spider.Policy: What spider.policy and spider.resourcePolicy return: 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

zig
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

zig
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

zig
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

zig
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

zig
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 with spider.policy, which wraps a bool or !bool function into this shape.

spider.rbac.policy

zig
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

zig
pub fn requirePolicy(comptime p: Policy) MiddlewareFn

The middleware behind a route's .policy.

spider.rbac.Deny

zig
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

zig
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):

zig
.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

zig
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:

zig
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

zig
pub const Resource = T

The type the set is about (T).

rbac.policySet.name

zig
pub const name: []const u8 = opts.name

The set's .name: the prefix of its policies' names ("ticket" in "ticket.update").

rbac.policySet.route

zig
pub fn route(comptime action: @TypeOf(.enum_literal)) Policy

The policy for a route: .policy = Tickets.route(.update).

rbac.policySet.can

zig
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

zig
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.