erify-authorization

erify_api Authorization Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erify-authorization" with this command: npx skills add allenlin90/eridu-services/allenlin90-eridu-services-erify-authorization

erify_api Authorization Patterns

This skill provides erify_api-specific authorization implementation patterns, centered on current isSystemAdmin

  • StudioMembership behavior, with planned RBAC patterns kept as future-reference only.

Read this skill for current erify_api authorization behavior first. Load the planned-RBAC sections only when the task is explicitly about future authorization design.

Related references

  • Authorization Guide

  • Architecture Overview

  • authentication-authorization-nestjs for broader auth guidance

  • backend-controller-pattern-nestjs for controller/decorator usage

Implementation Status

[!IMPORTANT] Not all patterns in this skill are implemented. Check the status below before using a pattern.

Pattern Status Notes

isSystemAdmin bypass ✅ Implemented AdminGuard checks this flag only

@AdminProtected() decorator ✅ Implemented Global guard in app.module.ts

@StudioProtected([roles])

✅ Implemented All 6 roles via StudioMembership (see role model below)

StudioGuard with membership check ✅ Implemented Validates studio membership + role via getAllAndOverride (method > class)

JSONB roles field on User ⏳ Planned Not in Prisma schema yet

JSONB permissions field on User ⏳ Planned Not in Prisma schema yet

ROLE_PERMISSIONS mapping ⏳ Planned AdminGuard does not expand roles

Granular permission strings (module:action ) ⏳ Planned Not implemented

Studio Role Model

StudioMembership.role has 6 values. Use this table as the canonical access reference:

Role Scope Can manage memberships

ADMIN

Full access — all studio features including membership management ✅ Yes

MANAGER

Full access — all studio features except membership management ❌ No

TALENT_MANAGER

Creator mapping only — catalog, roster, availability, show assignment ❌ No

DESIGNER

Dashboard, own tasks, own shifts only ❌ No

MODERATION_MANAGER

Dashboard, own tasks, own shifts only ❌ No

MEMBER

Dashboard, own tasks, own shifts only ❌ No

Backend endpoint role conventions

// Read endpoints — all studio members (no explicit roles = member+) @StudioProtected()

// Read/write endpoints for creator catalog/roster/availability and creator mapping ops @StudioProtected([STUDIO_ROLE.ADMIN, STUDIO_ROLE.MANAGER, STUDIO_ROLE.TALENT_MANAGER])

// Write endpoints open to manager-level ops (tasks, shifts, shows/task context) @StudioProtected([STUDIO_ROLE.ADMIN, STUDIO_ROLE.MANAGER])

// Admin-only (membership management, destructive ops) @StudioProtected([STUDIO_ROLE.ADMIN])

getAllAndOverride means method-level @StudioProtected always wins over class-level. The class sets the default; methods narrow or expand as needed.

Core Principles

  1. Separation of Concerns

Authentication (eridu_auth ): Handles user identity and JWT issuance

Authorization (erify_api ): Handles permissions and access control

IMPORTANT: Never add authorization claims to JWT payload. Keep JWTs minimal with identity claims only.

  1. Multi-Scope Access

Different user types have different access scopes:

User Type Access Scope Implementation

Creator Own shows only Via ShowMC relationship (DB internal)

Studio ADMIN All studio features + membership management Via StudioMembership role

Studio MANAGER All studio features (no membership management) Via StudioMembership role

Studio TALENT_MANAGER Creator mapping, catalog, roster, availability Via StudioMembership role

Studio DESIGNER / MODERATION_MANAGER Own tasks and shifts only Via StudioMembership role

Studio MEMBER Own tasks and shifts only Via StudioMembership role

Content Manager / System Manager Planned RBAC only Not implemented

2.1 Workflow Action Authorization

For workflow actions (for example show resolution actions), authorization must be scope-specific and stricter than generic edit checks.

Minimum rule set:

  • actor has required role in the target scope (for example studio admin),

  • resource belongs to the scoped entity (for example show belongs to :studioId ),

  • cross-scope/system-only fallback is not assumed for normal studio operations.

  1. Role-Based Permissions

Use roles for permission bundles, custom permissions for edge cases.

Permission Model

[!CAUTION] The following Permission Model section describes PLANNED (not yet implemented) patterns. The roles and permissions fields do NOT currently exist on the User model. The current AdminGuard only checks isSystemAdmin . Do NOT use this code in production without first adding schema migrations.

Database Schema

model User { isSystemAdmin Boolean @default(false) // Full access bypass roles Json @default("[]") // ["content_manager", "analyst"] permissions Json @default("[]") // ["users:read", "custom:feature"] }

Storage: JSONB in PostgreSQL (Prisma Json type)

Why JSONB:

  • Indexable with GIN for fast queries

  • Type-safe (Prisma parses to string[] )

  • Supports JSONB containment operators

Permission Format

Use module:action format:

  • users:read , users:write

  • shows:read , shows:write

  • reports:read , reports:export

Role Definitions

Define roles in AdminGuard or shared constants:

const ROLE_PERMISSIONS: Record<string, string[]> = { content_manager: ['shows:read', 'shows:write', 'schedules:read', 'schedules:write'], analyst: ['users:read', 'shows:read', 'reports:read'], support: ['users:read', 'tickets:read', 'tickets:write'], system_manager: [':'], // All permissions };

Effective Permissions

Effective permissions = Role permissions + Custom permissions

Example:

{ "roles": ["content_manager"], "permissions": ["reports:export"] }

Effective: shows:read , shows:write , schedules:read , schedules:write , reports:export

Implementation Patterns

[!CAUTION] All code examples below (AdminGuard, Controller pattern, Frontend integration, Role assignment) are PLANNED patterns — they reference user.roles , user.permissions , and ROLE_PERMISSIONS which do NOT yet exist. See the status table above.

AdminGuard Pattern (Planned)

@Injectable() export class AdminGuard implements CanActivate { private readonly ROLE_PERMISSIONS: Record<string, string[]> = { // Define role mappings here };

async canActivate(context: ExecutionContext): Promise<boolean> { const requiredPermissions = this.reflector.getAllAndOverride<string[]>( ADMIN_PERMISSIONS_KEY, [context.getHandler(), context.getClass()], ) || [];

const request = context.switchToHttp().getRequest&#x3C;AuthenticatedRequest>();
const user = await this.userService.getUserByExtId(request.user.ext_id);

// 1. System admin bypasses all checks
if (user.isSystemAdmin) return true;

// 2. Expand roles to permissions
const userRoles = (user.roles as string[]) || [];
const rolePermissions = userRoles.flatMap(role => this.ROLE_PERMISSIONS[role] || []);

// 3. Combine with custom permissions
const customPermissions = (user.permissions as string[]) || [];
const effectivePermissions = [...new Set([...rolePermissions, ...customPermissions])];

// 4. Check if user has ALL required permissions
return requiredPermissions.every(req => effectivePermissions.includes(req));

} }

Controller Pattern

@Controller('admin/users') export class AdminUserController { // Read-only access @AdminProtected('users:read') @Get() getUsers() { ... }

// Write access @AdminProtected('users:write') @Post() createUser() { ... }

// Multiple permissions required @AdminProtected(['users:read', 'users:write']) @Patch(':id') updateUser() { ... }

// System admin only (no specific permission) @AdminProtected() @Delete(':id') dangerousOperation() { ... } }

Frontend Integration Pattern

Expose effective permissions via /me endpoint:

@Get() async getMe(@CurrentUser() user: AuthenticatedUser) { const dbUser = await this.userService.getUserByExtId(user.ext_id);

// Expand roles to effective permissions const userRoles = (dbUser?.roles as string[]) || []; const rolePermissions = userRoles.flatMap(role => ROLE_PERMISSIONS[role] || []); const customPermissions = (dbUser?.permissions as string[]) || []; const effectivePermissions = [...new Set([...rolePermissions, ...customPermissions])];

return { ...user, isSystemAdmin: dbUser?.isSystemAdmin ?? false, roles: userRoles, permissions: effectivePermissions, // For UI permission checks }; }

Best Practices

✅ DO

  • Use roles for onboarding: Assign roles: ["content_manager"] instead of 50 individual permissions

  • Use custom permissions for edge cases: Add specific permissions on top of roles

  • Use granular permission strings: users:read , users:write (not admin:read )

  • Use isSystemAdmin for full access: Bypass all permission checks

  • Keep permission logic in backend: Frontend uses same permission strings

  • Document role definitions: Keep ROLE_PERMISSIONS mapping well-documented

  • Use JSONB for storage: Enables fast queries with GIN indexes

❌ DON'T

  • Don't add permissions to JWT: Keep JWTs minimal (identity only)

  • Don't create roles for every edge case: Use custom permissions instead

  • Don't use coarse permissions: admin:read is too broad

  • Don't duplicate permission logic: Backend and frontend should use same strings

  • Don't forget to expand roles: Always combine role + custom permissions

  • Don't use TEXT/CSV for storage: JSONB is superior for queries

Common Patterns

Pattern 1: Read/Write Separation

// Read endpoints @AdminProtected('module:read') @Get() list() { ... }

@AdminProtected('module:read') @Get(':id') get() { ... }

// Write endpoints @AdminProtected('module:write') @Post() create() { ... }

@AdminProtected('module:write') @Patch(':id') update() { ... }

@AdminProtected('module:write') @Delete(':id') delete() { ... }

Pattern 2: Scoped Access

// Studio-scoped access @Get('shows') @AdminProtected('shows:read') async getShows(@AuthUser() user) { // Filter by user's studio memberships const studioIds = user.studioMemberships.map(m => m.studioId); return this.showService.findByStudioRooms(studioIds); }

// Client-scoped access @Get('shows') @AdminProtected('shows:read') async getShows(@AuthUser() user, @Query('clientId') clientId?: string) { // Filter by user's client memberships or roles const clientIds = this.getAccessibleClients(user); return this.showService.findByClients(clientIds); }

// System-wide access @Get('shows') @AdminProtected('shows:read:all') async getAllShows() { // No filtering - system manager only return this.showService.findAll(); }

Pattern 3: Role Assignment

// Assign role to user await prisma.user.update({ where: { id: userId }, data: { roles: ['content_manager'] }, });

// Add custom permission await prisma.user.update({ where: { id: userId }, data: { roles: ['analyst'], permissions: ['reports:export'], }, });

Troubleshooting

Permission Denied (403)

  • Check user's isSystemAdmin flag

  • Check user's roles array

  • Check user's permissions array

  • Verify endpoint's @AdminProtected() requirements

  • Check AdminGuard logs for missing permissions

Role Not Expanding

  • Verify role name matches ROLE_PERMISSIONS mapping

  • Check for typos in role name

  • Ensure ROLE_PERMISSIONS is defined consistently

  • Consider extracting to shared constants file

Permissions Not Updating

  • Verify database update succeeded

  • Check if caching is enabled (invalidate cache)

  • Force token refresh (logout + login)

  • Check /me endpoint response

Related Skills

  • Authentication Authorization NestJS - Comprehensive auth patterns

  • Backend Controller Pattern NestJS - Controller patterns (admin, studio, me) with auth decorators

  • Data Validation - Input validation and serialization

Related Documentation

  • Authorization Guide (design-only; may be outdated vs current implementation)

  • Architecture Overview

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

data-validation

No summary provided by upstream source.

Repository SourceNeeds Review
General

task-template-builder

No summary provided by upstream source.

Repository SourceNeeds Review
General

jsonb-analytics-snapshot

No summary provided by upstream source.

Repository SourceNeeds Review
General

repository-pattern-nestjs

No summary provided by upstream source.

Repository SourceNeeds Review