Skip to content

Authorization -- Setup & Configuration

Enforcer-based authorization with RBAC, voters, and Casbin integration

Quick Reference

ItemValue
Package@venizia/ignis
ClassAuthorizeComponent
RuntimesBoth

Key Components

ComponentPurpose
AuthorizeComponentMain component validating authorization options and binding global config
AuthorizationEnforcerRegistrySingleton managing registered enforcers (mirrors AuthenticationStrategyRegistry)
CasbinAuthorizationEnforcerCasbin-backed enforcer (optional casbin peer dep)
AuthorizationProviderIProvider producing the authorize() middleware factory
authorizeStandalone function wrapping AuthorizationProvider.value()
AuthorizationRoleValue object for role identity with priority-based comparison
BaseFilteredAdapterAbstract casbin FilteredAdapter with template method pattern for query hooks
DrizzleCasbinAdapterDrizzle-based read-only FilteredAdapter using raw SQL queries
StringAuthorizationActionIAuthorizationComparable implementation for string actions (includes WILDCARD = '*')
StringAuthorizationResourceIAuthorizationComparable implementation for string resources
AbstractAuthRegistryShared base class for authentication strategy registry and authorization enforcer registry

Authorization Flow (7 Steps)

StepActionShort-circuits?
1Check Authorization.SKIP_AUTHORIZATION flagYes -- skip all
2Get authenticated user from contextYes -- 401 if missing
3Check role-based shortcuts (alwaysAllowRoles + allowedRoles)Yes -- allow if matched
4Execute voters (per-route)Yes -- DENY/ALLOW short-circuits
5Check if enforcers are registered; resolve enforcer (by name or default)Yes -- skip all if no enforcers registered
6Build or retrieve cached rulesNo
7Evaluate permission via enforcerYes -- 403 if denied

NOTE

Step 3 merges the global alwaysAllowRoles check and the per-route allowedRoles check into a single step. User roles are extracted once and checked against both lists.

NOTE

Step 5 has a safety fallback: if no enforcers are registered in the AuthorizationEnforcerRegistry, the middleware skips authorization entirely and calls next(). This prevents hard failures when authorization is configured on routes but no enforcer has been registered yet.

Authorization Constants

ConstantValueDescription
Authorization.RULES'authorization.rules'Context key for cached rules
Authorization.SKIP_AUTHORIZATION'authorization.skip'Context key to dynamically skip authorization
Authorization.ENFORCER'authorization.enforcer'Binding key prefix for enforcers

Authorization Actions

ConstantValue
AuthorizationActions.CREATE'create'
AuthorizationActions.READ'read'
AuthorizationActions.UPDATE'update'
AuthorizationActions.DELETE'delete'
AuthorizationActions.EXECUTE'execute'

AuthorizationActions.SCHEME_SET contains all valid actions. AuthorizationActions.isValid(input) checks membership.

Authorization Decisions

ConstantValueDescription
AuthorizationDecisions.ALLOW'allow'Grant access
AuthorizationDecisions.DENY'deny'Deny access
AuthorizationDecisions.ABSTAIN'abstain'No opinion -- fall through to next check

AuthorizationDecisions also provides comparison helpers that accept both strings and numbers:

MethodString checkNumber check
isAllow(input)input.toLowerCase() === 'allow'input > 0
isDeny(input)input.toLowerCase() === 'deny'input < 0
isAbstain(input)input.toLowerCase() === 'abstain'input === 0

AuthorizationDecisions.SCHEME_SET contains all valid decisions. AuthorizationDecisions.isValid(input) checks membership.

Authorization Enforcer Types

ConstantValueDescription
AuthorizationEnforcerTypes.CASBIN'casbin'Casbin-backed enforcer
AuthorizationEnforcerTypes.CUSTOM'custom'Custom enforcer implementation

AuthorizationEnforcerTypes.SCHEME_SET contains all valid types. AuthorizationEnforcerTypes.isValid(input) checks membership.

Casbin Enforcer Model Drivers

ConstantValueDescription
CasbinEnforcerModelDrivers.FILE'file'Load model from .conf file path
CasbinEnforcerModelDrivers.TEXT'text'Load model from inline string

CasbinEnforcerModelDrivers.SCHEME_SET contains all valid drivers. CasbinEnforcerModelDrivers.isValid(input) checks membership.

Casbin Enforcer Cached Drivers

ConstantValueDescription
CasbinEnforcerCachedDrivers.IN_MEMORY'in-memory'In-memory cache with periodic invalidation
CasbinEnforcerCachedDrivers.REDIS'redis'Redis-backed cache with TTL

CasbinEnforcerCachedDrivers.SCHEME_SET contains all valid drivers. CasbinEnforcerCachedDrivers.isValid(input) checks membership.

Casbin Rule Variants

ConstantValueDescription
CasbinRuleVariants.POLICY'policy'Database variant column value for permission rules
CasbinRuleVariants.GROUP'group'Database variant column value for role assignments
CasbinRuleVariants.P'p'Casbin line prefix for policy rules
CasbinRuleVariants.G'g'Casbin line prefix for grouping rules

CasbinRuleVariants.SCHEME_SET contains POLICY and GROUP (the DB variants). CasbinRuleVariants.isValid(input) checks membership against the DB variants.

NOTE

All constant classes follow the same pattern: static readonly values + SCHEME_SET: Set<string> + isValid(input): boolean. Each class also has a companion type alias generated via TConstValue<typeof ClassName> (e.g., TAuthorizationAction, TAuthorizationDecision, TCasbinRuleVariant).

Built-in Roles

ConstantIdentifierPriorityDescription
AuthorizationRoles.SUPER_ADMIN'999_super-admin'999Highest privilege
AuthorizationRoles.ADMIN'900_admin'900Administrator
AuthorizationRoles.USER'010_user'10Regular user
AuthorizationRoles.GUEST'001_guest'1Guest user
AuthorizationRoles.UNKNOWN_USER'000_unknown-user'0Unauthenticated fallback

Each built-in role is an AuthorizationRole instance. AuthorizationRoles.SCHEME_SET contains identifier strings. AuthorizationRoles.isValid(input) checks membership.

Import Paths

typescript
// Classes & functions
import {
  // Component & middleware
  AuthorizeComponent,
  AuthorizationProvider,
  authorize,

  // Registry
  AuthorizationEnforcerRegistry,

  // Enforcers
  CasbinAuthorizationEnforcer,

  // Adapters
  BaseFilteredAdapter,
  DrizzleCasbinAdapter,

  // Models
  AuthorizationRole,
  StringAuthorizationAction,
  StringAuthorizationResource,

  // Constants
  Authorization,
  AuthorizationActions,
  AuthorizationDecisions,
  AuthorizationRoles,
  AuthorizationEnforcerTypes,
  CasbinEnforcerModelDrivers,
  CasbinEnforcerCachedDrivers,
  CasbinRuleVariants,

  // Binding keys
  AuthorizeBindingKeys,
} from '@venizia/ignis';

// Types & interfaces
import type {
  // Core interfaces
  IAuthorizeOptions,
  IAuthorizationEnforcer,
  IAuthorizationSpec,
  IAuthorizationRequest,
  IAuthorizationRole,
  IAuthorizationComparable,

  // Casbin options
  ICasbinEnforcerOptions,
  ICasbinEnforcerCachedMemory,
  ICasbinEnforcerCachedRedis,

  // Adapter types
  IBaseFilteredAdapterEntities,
  ICasbinPolicyFilter,
  TBasePolicyRow,
  IDrizzleCasbinEntities,
  IDrizzleCasbinAdapterOptions,

  // Function & utility types
  TAuthorizeFn,
  TAuthorizationVoter,
  TAuthorizationConditions,
  TRegistryDescriptor,

  // Value types (from TConstValue)
  TAuthorizationAction,
  TAuthorizationDecision,
  TAuthorizationEnforcerType,
  TCasbinEnforcerCachedDriver,
  TCasbinEnforcerModelDriver,
  TCasbinRuleVariant,
} from '@venizia/ignis';

Setup

Authorization setup is a three-step process: bind global options, register the component, then register enforcers via the registry.

Step 1: Bind Global Options

Bind IAuthorizeOptions to configure global authorization behavior. This interface is minimal -- it only contains global settings, not enforcer-specific configuration.

typescript
import {
  AuthorizeBindingKeys,
  AuthorizationDecisions,
  IAuthorizeOptions,
} from '@venizia/ignis';

this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
  defaultDecision: AuthorizationDecisions.DENY,
  alwaysAllowRoles: ['999_super-admin'],
});

Step 2: Register the Component

typescript
import { AuthorizeComponent } from '@venizia/ignis';

this.component(AuthorizeComponent);

The component validates that IAuthorizeOptions is bound and extracts alwaysAllowRoles into a separate binding (AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES) for downstream consumers.

Step 3: Register Enforcers via Registry

Enforcer registration is separate from global options. Each enforcer is registered with its class, name, type, and options -- all co-located in one call.

typescript
import {
  AuthorizationEnforcerRegistry,
  AuthorizationEnforcerTypes,
  CasbinAuthorizationEnforcer,
  CasbinEnforcerModelDrivers,
  DrizzleCasbinAdapter,
} from '@venizia/ignis';
import path from 'node:path';

// Create adapter (Drizzle example)
const adapter = new DrizzleCasbinAdapter({
  dataSource,
  entities: {
    permission: { tableName: 'Permission', principalType: 'Permission' },
    role: { tableName: 'Role', principalType: 'Role' },
    policyDefinition: { tableName: 'PolicyDefinition', principalType: 'PolicyDefinition' },
    domain: { principalType: 'Organization' },
  },
});

AuthorizationEnforcerRegistry.getInstance().register({
  container: this,
  enforcers: [{
    enforcer: CasbinAuthorizationEnforcer,
    name: 'casbin',
    type: AuthorizationEnforcerTypes.CASBIN,
    options: {
      model: {
        driver: CasbinEnforcerModelDrivers.FILE,
        definition: path.resolve(__dirname, './security/rbac_with_domains_deny.conf'),
      },
      adapter,
      cached: {
        use: true,
        driver: 'redis',
        options: {
          connection: redisHelper,
          expiresIn: 5 * 60 * 1000, // 5 minutes
          keyFn: ({ user }) => `authz:policies:${user.userId}`,
        },
      },
      normalizePayloadFn: ({ user, action, resource }) => ({
        subject: `user_${user.userId}`,
        domain: `Organization_${user.organizationId}`,
        resource,
        action,
      }),
    },
  }],
});

Custom Enforcer

typescript
AuthorizationEnforcerRegistry.getInstance().register({
  container: this,
  enforcers: [{
    enforcer: MyCustomEnforcer,
    name: 'my-custom',
    type: AuthorizationEnforcerTypes.CUSTOM,
    options: { /* your enforcer-specific options */ },
  }],
});

Full Setup Example

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

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

    // Step 2: Register component
    this.component(AuthorizeComponent);

    // Step 3: Register enforcer(s) with co-located options
    const adapter = new DrizzleCasbinAdapter({ dataSource, entities: { ... } });

    AuthorizationEnforcerRegistry.getInstance().register({
      container: this,
      enforcers: [{
        enforcer: CasbinAuthorizationEnforcer,
        name: 'casbin',
        type: AuthorizationEnforcerTypes.CASBIN,
        options: {
          model: {
            driver: CasbinEnforcerModelDrivers.FILE,
            definition: path.resolve(__dirname, './security/rbac_model.conf'),
          },
          adapter,
          cached: { use: false },
        },
      }],
    });
  }
}

IMPORTANT

Authorization depends on authentication. Register AuthenticateComponent before AuthorizeComponent so that Authentication.CURRENT_USER is populated before authorization checks run.

NOTE

Enforcer-specific options (model, adapter, cached, normalizePayloadFn) are co-located with the enforcer registration in AuthorizationEnforcerRegistry.register(), not inside IAuthorizeOptions. This keeps enforcer configuration next to the enforcer class.

Configuration

IAuthorizeOptions

Global authorization settings. Bound to the container before registering AuthorizeComponent.

OptionTypeDefaultDescription
defaultDecisionTAuthorizationDecision--Required. Decision when enforcer returns ABSTAIN ('allow', 'deny', or 'abstain')
alwaysAllowRolesstring[][]Roles that bypass all authorization checks (global)
typescript
interface IAuthorizeOptions {
  defaultDecision: TAuthorizationDecision;
  alwaysAllowRoles?: string[];
}

ICasbinEnforcerOptions

Casbin-specific options, provided per-enforcer via AuthorizationEnforcerRegistry.register().

OptionTypeDefaultDescription
model{ driver, definition }--Required. Casbin model definition (file path or inline text)
cached{ use: false } | { use: true, driver, options }--Required. Caching configuration
adapterAdapter--Casbin adapter instance (e.g., DrizzleCasbinAdapter)
normalizePayloadFn(opts) => { subject, resource, action, domain? }--Normalize subject/resource/action before evaluation
typescript
interface ICasbinEnforcerOptions<
  E extends Env = Env,
  TAction = string,
  TResource = string,
  TAdapter = Adapter,
> {
  model:
    | { driver: 'file'; definition: string }
    | { driver: 'text'; definition: string };

  cached:
    | { use: false }
    | { use: true; driver: 'in-memory'; options: { expiresIn: number } }
    | { use: true; driver: 'redis'; options: {
        connection: DefaultRedisHelper;
        expiresIn: number;
        keyFn: (opts: { user: { principalType: string } & IAuthUser }) => ValueOrPromise<string>;
      } };

  adapter?: TAdapter;

  normalizePayloadFn?(opts: {
    user: IAuthUser;
    action: TAction;
    resource: TResource;
    context: TContext<E, string>;
  }): {
    subject: string;
    resource: string;
    action: string;
    domain?: string;
  };
}

NOTE

cached.options.expiresIn must be >= 10,000 ms (10 seconds). Values below this threshold cause a validation error (MIN_EXPIRES_IN = 10_000).

Cache Configuration Types

The cached field is a discriminated union:

typescript
// No caching
interface { use: false }

// In-memory cache (CachedEnforcer with periodic invalidation timer)
interface ICasbinEnforcerCachedMemory {
  driver: 'in-memory';
  options: { expiresIn: number };
}

// Redis cache (store/retrieve policy lines from Redis)
interface ICasbinEnforcerCachedRedis {
  driver: 'redis';
  options: {
    connection: DefaultRedisHelper;
    expiresIn: number;
    keyFn: (opts: { user: { principalType: string } & IAuthUser }) => ValueOrPromise<string>;
  };
}

IAuthorizationSpec (Route-level)

OptionTypeDefaultDescription
actionTAction--Required. Action being performed (e.g., 'read', 'create')
resourceTResource--Required. Resource being accessed (e.g., 'Article', 'User')
conditionsTAuthorizationConditions--Key-value conditions for ABAC (strict equality)
allowedRolesstring[]--Roles that bypass enforcer for this specific route
votersTAuthorizationVoter[]--Custom voter functions for this specific route
typescript
interface IAuthorizationSpec<E extends Env = Env, TAction = string, TResource = string> {
  action: TAction;
  resource: TResource;
  conditions?: TAuthorizationConditions;
  allowedRoles?: string[];
  voters?: TAuthorizationVoter<E, TAction, TResource>[];
}

NOTE

TAction and TResource default to string but can accept IAuthorizationComparable implementations (e.g., StringAuthorizationAction) for custom comparison logic.

TAuthorizationConditions

Key-value conditions for attribute-based access control. Values are compared with strict equality (===).

typescript
type TAuthorizationConditions<
  KeyType extends string | symbol = string | symbol,
  ValueType = string | number | boolean | null,
> = Record<KeyType, ValueType>;

TAuthorizationVoter

Function type for voter callbacks:

typescript
type TAuthorizationVoter<
  E extends Env = Env,
  TAction = string,
  TResource = string,
> = (opts: {
  user: IAuthUser;
  action: TAction;
  resource: TResource;
  context: TContext<E, string>;
}) => ValueOrPromise<TAuthorizationDecision>;

TAuthorizeFn

Function type for the authorize() middleware factory:

typescript
type TAuthorizeFn<E extends Env = Env, TAction = string, TResource = string> = (opts: {
  spec: IAuthorizationSpec<E, TAction, TResource>;
  enforcerName?: string;
}) => MiddlewareHandler;

Binding Keys

KeyConstantTypeDescription
@app/authorize/optionsAuthorizeBindingKeys.OPTIONSIAuthorizeOptionsGlobal authorization options
@app/authorize/always-allow-rolesAuthorizeBindingKeys.ALWAYS_ALLOW_ROLESstring[]Auto-bound by component if present in options
@app/authorize/enforcers/{name}/optionsAuthorizeBindingKeys.enforcerOptions(name)ICasbinEnforcerOptions | unknownPer-enforcer options, auto-bound by registry
typescript
class AuthorizeBindingKeys {
  static readonly OPTIONS = '@app/authorize/options';
  static readonly ALWAYS_ALLOW_ROLES = '@app/authorize/always-allow-roles';

  static enforcerOptions(name: string): string {
    return `@app/authorize/enforcers/${name}/options`;
  }
}

NOTE

AuthorizeBindingKeys.enforcerOptions(name) is called automatically by AuthorizationEnforcerRegistry.register() when options is provided. The CasbinAuthorizationEnforcer injects its options from AuthorizeBindingKeys.enforcerOptions('casbin').

Context Variables

The authorization module extends Hono's ContextVariableMap for type-safe context access. The full augmentation is defined in auth/context-variables.ts and covers both authentication and authorization:

typescript
declare module 'hono' {
  interface ContextVariableMap {
    // Authentication
    [Authentication.CURRENT_USER]: IAuthUser;
    [Authentication.AUDIT_USER_ID]: IdType;
    [Authentication.SKIP_AUTHENTICATION]: boolean;

    // Authorization
    [Authorization.RULES]: unknown;
    [Authorization.SKIP_AUTHORIZATION]: boolean;
  }
}

Authorization-Specific Variables

KeyConstantTypeDescription
'authorization.rules'Authorization.RULESunknownCached rules built by the enforcer. Type depends on enforcer implementation.
'authorization.skip'Authorization.SKIP_AUTHORIZATIONbooleanSet to true to dynamically skip authorization for this request.

Authentication Variables Used by Authorization

KeyConstantTypeUsed for
'authentication.currentUser'Authentication.CURRENT_USERIAuthUserRead in step 2 to get authenticated user
'authentication.auditUserId'Authentication.AUDIT_USER_IDIdTypeAvailable for audit logging

IAuthUser Interface

The IAuthUser interface (from the authenticate module) is the user object available during authorization:

typescript
interface IAuthUser {
  userId: IdType;  // IdType = number | string | bigint
  [extra: string | symbol]: any;
}

The authorization middleware accesses user.roles (for role extraction) and user.principalType (for enforcer-based evaluation) via the index signature.

IJWTTokenPayload Interface

When using JWT authentication, the full token payload extends IAuthUser:

typescript
interface IJWTTokenPayload extends JWTPayload, IAuthUser {
  userId: IdType;
  roles: { id: IdType; identifier: string; priority: number }[];
  clientId?: string;
  provider?: string;
  email?: string;
  name?: string;
  [extra: string | symbol]: any;
}

NOTE

IdType = number | string | bigint is defined in @/base/models/common/types.

Relationship with Authentication

Authorization runs after authentication in the middleware chain. Both REST and gRPC controllers ensure the correct ordering:

  1. Authentication middleware is injected first (from authenticate config)
  2. Authorization middleware is injected second (from authorize config)
  3. Custom middleware is injected last (from middleware config -- REST only)

This means Authentication.CURRENT_USER is always available when the authorization middleware executes.

REST Controllers

The AbstractRestController.buildRouteMiddlewares() method builds the middleware array from route config. The getRouteConfigs() method calls buildRouteMiddlewares() internally and wraps it into a Hono route definition:

typescript
// In AbstractRestController
buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
  const { authenticate = {}, authorize, ...restConfig } = opts.configs;
  const mws = [];

  // 1. Authenticate middleware (first)
  if (strategies.length > 0) {
    mws.push(authenticateFn({ strategies, mode }));
  }

  // 2. Authorize middleware (second) — supports single or array
  if (authorize) {
    const specs = Array.isArray(authorize) ? authorize : [authorize];
    for (const spec of specs) {
      mws.push(authorizeFn({ spec }));
    }
  }

  // 3. Custom middleware (last)
  if (restConfig.middleware) { ... }

  return { restConfig, security, mws };
}

gRPC Controllers

The AbstractGrpcController.buildRpcMiddlewares() method provides symmetric authorization support for gRPC routes. Authorization specs are applied the same way as REST:

typescript
// In AbstractGrpcController
buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
  const { configs } = opts;
  const mws = [];

  // 1. Authenticate middleware
  if (configs.authenticate) { ... }

  // 2. Authorize middleware — same pattern as REST
  if (configs.authorize) {
    const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
    for (const spec of specs) {
      const authzMw = authorizeFn({ spec });
      mws.push((context, next) => authzMw(context, next));
    }
  }

  return mws;
}

IAuthRouteConfig

Extended route config that supports both authentication and authorization:

typescript
interface IAuthRouteConfig extends HonoRouteConfig {
  authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
  authorize?: IAuthorizationSpec | IAuthorizationSpec[];
}

When authorize is an array, each spec creates a separate middleware. All must pass for the handler to execute.

Per-Route Configuration in CRUD Factory

CRUD factory routes support both authentication and authorization configuration:

typescript
ControllerFactory.defineCrudController({
  entity: Article,
  repository: { name: 'ArticleRepository' },
  controller: {
    name: 'ArticleController',
    basePath: '/articles',
  },
  authenticate: { strategies: [Authentication.STRATEGY_JWT], mode: AuthenticationModes.ANY },
  authorize: { action: AuthorizationActions.READ, resource: 'Article' },
  routes: {
    // Skip both auth for public read
    find: { authenticate: { skip: true } },
    // Override authorization for delete
    deleteById: {
      authorize: { action: AuthorizationActions.DELETE, resource: 'Article' },
    },
    // Skip only authorization
    count: { authorize: { skip: true } },
  },
});

Priority rules:

  1. authenticate: { skip: true } -- skips both authentication AND authorization
  2. authorize: { skip: true } -- skips only authorization (authentication still runs)
  3. Per-route authorize overrides controller-level authorize
  4. No per-route config -- inherits controller-level config

Per-Route Auth Types

typescript
/** Per-route authorization config: { skip: true }, single spec, or array of specs. */
type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];

/** Per-route auth config. Endpoint config takes precedence over controller-level config. */
type TRouteAuthConfig = {
  authenticate?: TRouteAuthenticateConfig;
  authorize?: TRouteAuthorizeConfig;
};

See Also