Keelv0.86.0
Docs / Authorization

Where authentication answers who you are, authorization answers what you're allowed to do. Keel gives you gates (ad-hoc abilities) and policies (abilities grouped per model) — a compact authorization layer.

The current user is resolved from auth().user() by default, so authorization composes with the session auth you already have.

Gates

A gate is a named ability with a callback that receives the user and whatever you pass to the check:

import { define, can, authorize } from "@shaferllc/keel/core";

// register once, at boot (e.g. in a service provider):
define("update-post", (user, post) => post.authorId === user.id);
define("access-admin", (user) => user.role === "admin");

// check anywhere:
if (await can("update-post", post)) {
  // …
}

await authorize("update-post", post); // throws a 403 ForbiddenException if denied

can(ability, ...args) returns a boolean; cannot(...) is its negation; authorize(...) throws a 403 when denied (the HTTP kernel renders it).

Policies

For a model with several abilities, group them in a policy class — one method per ability — and register it. can("update", post) then routes to PostPolicy.update(user, post) automatically, by the argument's class:

import { policy, can } from "@shaferllc/keel/core";

class PostPolicy {
  view(user, post) {
    return post.published || post.authorId === user.id;
  }
  update(user, post) {
    return post.authorId === user.id;
  }
  delete(user, post) {
    return user.admin || post.authorId === user.id;
  }
}

policy(Post, PostPolicy); // register the class (or an instance)

await can("view", post);            // → PostPolicy.view(user, post)
await authorize("delete", post);    // → PostPolicy.delete(user, post) or 403

A policy is a plain class — no base class, no framework glue. The method name is the ability; the first argument to the check is the model.

Admin bypass (before hooks)

Register a gateBefore callback to decide checks up front — return a boolean to short-circuit, or undefined to fall through to the gate/policy. Perfect for a super-admin:

import { gateBefore } from "@shaferllc/keel/core";

gateBefore((user) => (user.role === "superadmin" ? true : undefined));

A gateAfter callback brackets the other end: it runs after the gate/policy and receives the result, returning a boolean to override it or undefined to keep it. Use it to audit every decision, or to veto late:

import { gateAfter } from "@shaferllc/keel/core";

gateAfter((user, ability, args, result) => {
  log.info("authz", { user: user.id, ability, result });
  return undefined; // keep the original decision
});

In a controller

export class PostController {
  async update(c: Ctx) {
    const post = await Post.findOrFail(param("id"));
    await authorize("update", post); // 403 unless allowed
    // … safe to proceed
  }
}

Checking a specific user

can/authorize use the current user. To check someone else (background jobs, tests, impersonation), use the For variants:

import { canFor, authorizeFor } from "@shaferllc/keel/core";

await canFor(otherUser, "update-post", post);
await authorizeFor(otherUser, "update-post", post);

Resolving the current user differently (token auth instead of session)? setUserResolver(() => currentUserSomehow()).

API reference

define(ability, callback)

define(ability: string, callback: (user, ...args) => boolean | Promise<boolean>): void

Registers a gate. The callback receives the resolved user and the check arguments.

policy(model, impl)

policy(model: Constructor, impl: Policy | (new () => Policy)): void

Registers a policy (class or instance) for a model. can(ability, instance) routes to impl[ability](user, instance) when the ability matches a method.

can(ability, ...args) / cannot(...)

can(ability: string, ...args): Promise<boolean>

Whether the current user is allowed. cannot is the negation. Policy (matching model argument) is tried first, then a gate; unknown abilities deny.

authorize(ability, ...args)

authorize(ability: string, ...args): Promise<void>

Throws a 403 ForbiddenException unless allowed.

canFor(user, ...) / authorizeFor(user, ...)

The can / authorize pair for an explicit user rather than the current one.

gateBefore(callback)

gateBefore(callback: (user, ability, args) => boolean | undefined | Promise<…>): void

Runs before every check; a boolean short-circuits, undefined falls through.

setUserResolver(resolver) / clearAuthorization()

Override how the current user is resolved (default auth().user()), and reset all gates/policies/hooks (a test helper).

Interfaces & types

GateCallback

type GateCallback = (user: unknown, ...args: unknown[]) => boolean | Promise<boolean>

BeforeCallback

type BeforeCallback = (user, ability: string, args: unknown[]) => boolean | undefined | Promise<boolean | undefined>