Skip to content

Authorization

AuthorizeComponent wires up enforcer-based route authorization - RBAC through Casbin (with an optional multi-tenant domain-scoped model), voters, and role shortcuts - evaluated by the authorize() middleware after authentication.

In one example

The recommended setup: scoped Casbin RBAC backed by one PolicyDefinition edge table, then a protected route.

typescript
import {
  AuthorizeBindingKeys, AuthorizeComponent, AuthorizationDecisions, AuthorizationEnforcerRegistry,
  AuthorizationEnforcerTypes, CasbinAuthorizationEnforcer, CasbinEnforcerModelDrivers,
  ScopedCasbinAdapter, CASBIN_RBAC_DOMAIN_SCOPED_MODEL, BaseApplication, IAuthorizeOptions,
} from '@venizia/ignis';

export class Application extends BaseApplication {
  preConfigure() {
    // 1. Global options
    this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
      defaultDecision: AuthorizationDecisions.DENY,
      alwaysAllowRoles: ['999_super-admin'],
    });

    // 2. Component (validates options, binds alwaysAllowRoles)
    this.component(AuthorizeComponent);

    // 3. Enforcer, registered by class + name + type + co-located options
    const adapter = new ScopedCasbinAdapter({
      dataSource, // a BasePostgresDataSource instance
      entities: {
        policyDefinition: { tableName: 'PolicyDefinition', schemaName: 'identity' },
        permission: { tableName: 'Permission', schemaName: 'identity' },
        principals: { user: 'User', role: 'Role' },
        domainTypes: ['Merchant'],
        softDelete: { use: true, columnName: 'deleted_at' },
      },
    });

    AuthorizationEnforcerRegistry.getInstance().register({
      container: this,
      enforcers: [{
        enforcer: CasbinAuthorizationEnforcer,
        name: 'casbin',
        type: AuthorizationEnforcerTypes.CASBIN,
        options: {
          model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL },
          isScoped: true,
          adapter,
          cached: { use: false },
        },
      }],
    });
  }
}
typescript
// Inside a controller's binding() - a route protected by authentication + authorization
const DELETE_ARTICLE_CONFIG = {
  path: '/articles/{id}',
  method: 'delete',
  authenticate: { strategies: [Authentication.STRATEGY_JWT] },
  authorize: { action: AuthorizationActions.DELETE, resource: 'Article' },
  responses: jsonResponse({ description: 'Deleted article', schema: ArticleSchema }),
} as const;

How it works

  • Enforcer-based and pluggable. authorize({ spec }) returns Hono middleware built by AuthorizationProvider. It resolves an IAuthorizationEnforcer from AuthorizationEnforcerRegistry by name (default: the first registered) - swap CasbinAuthorizationEnforcer for a custom class without touching route configs.
  • Runs after authentication. The middleware reads Authentication.CURRENT_USER from the Hono context, so AuthenticateComponent must run first and the route needs an authenticate config alongside authorize.
  • No enforcers registered = no-op. If AuthorizationEnforcerRegistry.hasEnforcers() is false, the middleware calls next() and skips authorization entirely - useful during incremental rollout, dangerous if you forget to register an enforcer in production.
  • Casbin's scoped RBAC model is the recommended engine. CASBIN_RBAC_DOMAIN_SCOPED_MODEL + isScoped: true + ScopedCasbinAdapter reads one principal's role/permission/domain edges (plus the shared role/resource/action/domain hierarchy) from a single PolicyDefinition table, and supports multi-tenant grants scoped to SYSTEM_WIDE, ANY_MEMBER, or a specific <Type>_<id> domain.
  • Per-request enforcers, cached lines. Each Casbin evaluation borrows an isolated enforcer from an internal pool, loads that user's policy lines into it, and evaluates - the datasource query only runs on cache miss (or always, if cached.use: false).

Pipeline (7 steps, short-circuits marked)

#StepShort-circuits when
1Authorization.SKIP_AUTHORIZATION checkSet -> next()
2Read Authentication.CURRENT_USERMissing -> 401
3Role shortcuts (alwaysAllowRoles + allowedRoles)Match -> next()
4Voters (per-route)ALLOW/DENY -> next() / 403
5Resolve enforcerNone registered -> next()
6Build/cache rules (+ resolve domain, if any)-
7enforcer.evaluate()DENY/ABSTAIN-as-deny -> 403

Common tasks

Secure a route. Add authorize: { action, resource } next to authenticate in a route config or @get/@post decorator.

typescript
authorize: { action: AuthorizationActions.READ, resource: 'Article' }

Bypass the enforcer for trusted roles. alwaysAllowRoles (global, in IAuthorizeOptions) or allowedRoles (per-route spec) skip straight to next() once a matching role is found.

typescript
authorize: { action: AuthorizationActions.DELETE, resource: 'Article', allowedRoles: [AuthorizationRoles.ADMIN.identifier] }

Add custom logic before the enforcer. A voter returns ALLOW/DENY/ABSTAIN; the first non-ABSTAIN decision wins.

typescript
const ownerVoter: TAuthorizationVoter = async ({ user, context }) =>
  (await articleService.findById({ id: context.req.param('id') }))?.authorId === user.userId
    ? AuthorizationDecisions.ALLOW
    : AuthorizationDecisions.ABSTAIN;

Scope a grant to a tenant. Declare where the domain comes from per route (or globally via IAuthorizeOptions.domainResolver).

typescript
authorize: { action: 'read', resource: 'Order', domain: { from: 'param', key: 'merchantId', type: 'Merchant' } }

Reference a model instead of a hardcoded string. @model({ settings: { authorize: { principal } } } }) auto-populates Model.AUTHORIZATION_SUBJECT.

typescript
authorize: { action: AuthorizationActions.READ, resource: Article.AUTHORIZATION_SUBJECT }

See also

Files: