Skip to main content
The @ooneex/permission component is a fine-grained access control layer built on CASL. You describe what is allowed as a set of abilities — pairs of an action (read, update, delete, …) and a subject (User, Article, all, …) — then ask whether the current user can or cannot perform an action. Permissions are written as classes that extend the abstract Permission base, registered with the container, and attached to routes so authorization runs before your handler.

Why this component

  • Ability-based, not flag-based. Authorize concrete action + subject pairs instead of scattering boolean role checks across your code.
  • User-aware. setUserPermissions(context) grants abilities from the request context — the same permission class adapts to admins, owners, and guests.
  • Field-level control. can() and cannot() take an optional field, so you can allow reading name while forbidding password.
  • Type-safe subjects and actions. EPermissionAction ships 60+ actions and EPermissionSubject common subjects, with literal types you can extend per domain.
  • Container-managed. Register a permission class with a decorator and resolve it from the container or wire it to a route.

How it works

A permission is a class extending Permission. You implement three methods, then build() compiles the rules into a CASL ability you query with can/cannot.
MethodPurpose
allow()Declare baseline abilities with this.ability.can(...) / this.ability.cannot(...). Returns this.
setUserPermissions(context)Add abilities derived from the request context (the user, their roles, ownership). Returns this.
check(context)Custom gate run before the route handler; return false to deny outright.
build()Compile declared rules into the ability. Must run before can/cannot. Returns this.
can(action, subject, field?)Whether the action is allowed on the subject (optionally a field).
cannot(action, subject, field?)Whether the action is forbidden on the subject (optionally a field).
The lifecycle is a fluent chain: declare context-driven rules, declare baseline rules, build, then query.
const permission = new ArticlePermission();

permission
  .setUserPermissions(context) // abilities from the current user
  .allow() // baseline abilities
  .build(); // compile before checking

if (permission.can(EPermissionAction.EDIT, "Article")) {
  // proceed
}
can and cannot throw a PermissionException (NOT_BUILT) if called before build(). The MANAGE action and the all subject act as wildcards — this.ability.can("manage", "all") grants everything.

Decorator and usage

@decorator.permission()

Registers a permission class with the container. It accepts an optional scope (defaults to singleton).
import type { ContextType } from "@ooneex/controller";
import { decorator, EPermissionAction, Permission } from "@ooneex/permission";

@decorator.permission()
export class ArticlePermission extends Permission {
  public allow(): this {
    this.ability.can(EPermissionAction.READ, "Article");
    this.ability.can(EPermissionAction.VIEW, "Article");
    return this;
  }

  public setUserPermissions(context: ContextType): this {
    const { user } = context;
    if (!user) return this;

    // Authenticated users can create and edit articles
    this.ability.can(EPermissionAction.CREATE, "Article");
    this.ability.can(EPermissionAction.EDIT, "Article");

    // Admins manage everything
    if (user.roles.includes("ROLE_ADMIN")) {
      this.ability.can(EPermissionAction.MANAGE, "all");
    }

    return this;
  }

  public check(context: ContextType): boolean {
    return true;
  }
}
Attach the class to a route and authorization runs automatically before the handler:
import type { ContextType, IController } from "@ooneex/controller";
import { Route } from "@ooneex/routing";
import { ArticlePermission } from "@/permissions/ArticlePermission";

@Route.http({
  name: "api.articles.update",
  path: "/api/articles/:id",
  method: "PUT",
  permission: ArticlePermission,
})
class ArticleUpdateController implements IController {
  public async index(context: ContextType) {
    // Reached only when the permission allows it
  }
}
You can also evaluate a permission manually — useful for ownership checks the route-level gate can’t know about:
const permission = new ArticlePermission();

permission
  .setUserPermissions(context)
  .allow()
  .build();

if (permission.cannot(EPermissionAction.DELETE, "Article")) {
  return context.response.exception("Not authorized", { status: 403 });
}
Restrict to specific fields by passing a field name:
permission.can(EPermissionAction.READ, "User", "name"); // true
permission.can(EPermissionAction.READ, "User", "password"); // false

Exceptions

The component throws PermissionException when an ability is queried before the permission has been built. It carries a machine-readable key, a human-readable message, and a data object.
KeyWhen
NOT_BUILTcan() or cannot() is called before build().
import { PermissionException } from "@ooneex/permission";

try {
  permission.can(EPermissionAction.READ, "Article");
} catch (error) {
  if (error instanceof PermissionException) {
    logger.error(`Permission error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}

Best practices

  • Always build() before checking. Compile the ability once after declaring rules; querying before throws PermissionException (NOT_BUILT).
  • Keep the chain consistent. Run setUserPermissions() before allow() and build() last so context-driven rules are in place.
  • Use the action and subject enums. Prefer EPermissionAction and EPermissionSubject over raw strings to stay type-safe and consistent.
  • Reserve manage / all for admins. The wildcard grants every action on every subject — scope it to trusted roles only.
  • Move ownership logic into the permission. Pass owner data through the context and decide inside setUserPermissions() rather than in the controller.
  • Deny early in check(). Use it for coarse gates (method, IP, headers) and let can/cannot handle the fine-grained decisions.
  • Throw PermissionException with a stable key. Keep keys constant and put variable detail in data.

CLI command

Scaffold a permission class and its test file with the generator. It writes the class under modules/<module>/src/permissions/<Name>Permission.ts and installs @ooneex/permission if it is missing.
# Interactive: prompts for the name
ooneex permission:create

# Provide the name
ooneex permission:create --name=EditArticle

# Target a module and overwrite
ooneex permission:create --name=EditArticle --module=blog --override
OptionDescriptionDefault
--namePermission class name. The Permission suffix is appended automatically.Prompted if omitted
--moduleTarget module the class is generated into.shared
--overrideOverwrite an existing class without prompting.false
The generated class is a ready-to-fill stub with the three methods and inline examples:
import type { ContextType } from "@ooneex/controller";
import { decorator, Permission } from "@ooneex/permission";

@decorator.permission()
export class EditArticlePermission extends Permission {
  public allow(): this {
    // Example: Add permissions using this.ability.can()
    // this.ability.can("read", "YourEntity");
    // this.ability.can(["read", "update"], "YourEntity", { userId: user.id });

    return this;
  }

  public setUserPermissions(context: ContextType): this {
    // Example: Grant full access to admins
    // const { user } = context;
    //
    // if (!user) {
    //   return this;
    // }
    //
    // const { roles } = user;
    // if (roles.includes("ROLE_ADMIN")) {
    //   this.ability.can("manage", "all");
    //   return this;
    // }

    return this;
  }

  public check(context: ContextType): boolean {
    // Example: Restrict access based on request method
    // const { method } = context;
    // if (method === "DELETE") {
    //   return false;
    // }

    return true;
  }
}
See permission:create for the full command reference.

Use with Claude and Codex

The generator ships a matching permission:create skill. It runs the scaffold and then guides your AI agent through completing the permission — implementing allow() with ability rules and setUserPermissions() with role-based logic. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:
Prompt
Create a permission that controls who can edit articles.
For example, the prompt above maps to permission:create --name=EditArticle, then implements allow() and setUserPermissions() so only the right users can edit articles.