Enforce ownership and role checks on the server even when the UI hides forbidden actions.
Course navigationPhase 19⌄
Authorization, Ownership, RBAC & Permissions
Enforce resource ownership, workspace membership, roles, and permissions on the API with server-side policies.
Phase outcome and project increment
Learning outcome
Enforce resource ownership, workspace membership, roles, and permissions on the API with server-side policies.
Concrete deliverable
Permission model + guards/policies protecting TaskFlow workspace/task actions with deliberate 401/403/404 semantics.
Principles to keep while coding
Model permissions around actions and resources instead of scattering role-name comparisons everywhere.
Check workspace membership before allowing resource operations tied to that workspace.
Use least privilege: a role should receive only the capabilities its workflows require.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
List TaskFlow actions such as workspace.read, task.create, task.update, member.manage.
- 02
Map roles to capabilities without scattering role-name if statements across controllers.
- 03
Load membership for the relevant workspace and treat it as authorization evidence.
- 04
Enforce owner/member/role checks in server-side policy code.
- 05
Decide when 403 reveals too much and when a 404-style response better protects resource existence.
- 06
Write a matrix test covering at least owner, member, outsider, unauthenticated caller, and missing resource.
Failures you should know how to diagnose
Authorization only in frontend
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Role strings checked inconsistently in every route
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Checking membership after performing the mutation
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
IDOR: trusting a resource ID without proving caller access
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Lessons
The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.
Authorization Mental Model and Threats
Authorization Mental Model & Threats Separate identity from access and understand broken-access-control failure modes Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Auth
Link Supabase Identity to TaskFlow User
Link Supabase Identity to TaskFlow User Create a safe bridge between external identity and application-domain authorization data Phase 19 - Authorization, Ownership, RBAC & Permissions Volum
RBAC Roles and Permission Matrix
RBAC, Roles & Permission Matrix Map workspace roles to explicit operations without scattering role checks Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authentication &
NestJS Permission Metadata and Guards
NestJS Permission Metadata & Guards Use Reflector metadata and guards for coarse workspace authorization Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authentication &
Object Level Authorization and IDOR Defense
Object-Level Authorization & IDOR Defense Scope actual database lookups by authenticated relationships Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authentication & Se
Ownership Assignment and Service Level Policies
Ownership, Assignment & Service-Level Policies Enforce relationship-aware policies where trusted resource context exists Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - A
401 403 404 and Error Contract
401, 403, 404 & Error Contract Use status semantics without leaking inaccessible resource existence Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authentication & Secur
Multi Tenant Query Scoping with Prisma
Multi-Tenant Query Scoping with Prisma Make every collection and item query operate inside the caller authorized dataset Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - A
Membership Management and Privilege Escalation
Membership Management & Privilege Escalation Protect the role and membership data that defines authorization itself Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authen
Authorization Testing and Security Verification
Authorization Testing & Security Verification Test vertical and horizontal boundaries directly against the API Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authenticat
Enterprise Authorization Patterns
Enterprise Authorization Patterns Transfer RBAC, ownership, ABAC and relationship reasoning to real business systems Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authe
Hands On Lab and Phase Exit
Hands-On Lab & Phase Exit Prove TaskFlow authorization using multiple users and workspaces Phase 19 - Authorization, Ownership, RBAC & Permissions Volume 05 - Authentication & Security Archi
Guided Build Lab
Purpose Enforce resource ownership, workspace membership, roles, and permissions on the API with server-side policies.
Debugging Workshop
Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.
Phase Synthesis & Independent Challenge
Mission Rebuild the capability of Phase 19 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?
Mastery Capstone Lab
Integrate Phase 19 without following a transcript.
Exercises that increase independence
List TaskFlow actions such as workspace.read, task.create, task.update, member.manage.
Load membership for the relevant workspace and treat it as authorization evidence.
reproduce and diagnose — Authorization only in frontend.
deliver Permission model + guards/policies protecting TaskFlow workspace/task actions with deliberate 401/403/404 semantics.
Can model permission checks by action/resource and Can test authorization matrixes.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
Reference files
Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.
reference/TaskFlow_Authorization_Snapshot/AUTHORIZATION_MATRIX.md
# TaskFlow Phase 19 Authorization Matrix | Operation | OWNER | ADMIN | MEMBER | |---|---:|---:|---:| | Read workspace/projects/tasks | yes | yes | yes | | Create task | yes | yes | yes | | Update any task | yes | yes | no | | Update assigned task | yes | yes | yes | | Delete task | yes | yes | no | | Create/update project | yes | yes | no | | Manage members | yes | yes | no | | Manage workspace settings | yes | no | no | Notes: - This is the course policy, not a universal RBAC model. - Object-level checks still apply after role-to-permission mapping. - A MEMBER being able to update assigned tasks is relationship-based, not just role-based. - The server rechecks authorization on every request.
reference/TaskFlow_Authorization_Snapshot/SCHEMA_CHANGE_AND_LINKING.md
# Linking Supabase Auth identity to TaskFlow User Phase 18 gives NestJS a verified Supabase subject (`request.user.id`). Phase 19 must map that external identity to the domain `User` row used by Membership and Task relations. Recommended migration: 1. Add `authSubject String? @unique` to `User`. 2. Run a migration. 3. Sign in with the development Supabase account and inspect `/auth/me` to obtain the verified `sub`. 4. Link that `sub` to the intended TaskFlow User row using a trusted admin/dev path (Prisma Studio, SQL, or a one-time script). 5. Do not accept `authSubject` from an ordinary browser request body. 6. After all real users are linked, consider making the column required in a later migration.
reference/TaskFlow_Authorization_Snapshot/SOURCE_NOTES.txt
Phase 19 source notes - verified September 18, 2026 NestJS Authorization https://docs.nestjs.com/security/authorization NestJS Execution Context / Reflector metadata https://docs.nestjs.com/fundamentals/execution-context OWASP Authorization Cheat Sheet https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html OWASP Business Logic Security Cheat Sheet https://cheatsheetseries.owasp.org/cheatsheets/Business_Logic_Security_Cheat_Sheet.html Core course locks remain unchanged: - NestJS 12.0.3 - Prisma 7.9.1 - Supabase Auth stack from Phase 17
reference/TaskFlow_Authorization_Snapshot/apps/api/prisma/schema.phase19.prisma.fragment
model User {
id String @id @default(uuid())
authSubject String? @unique
email String @unique
displayName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memberships Membership[]
assignedTasks Task[] @relation("TaskAssignee")
}
// Phase 19 migration strategy:
// 1. Add authSubject as nullable + unique.
// 2. Link the development Supabase auth subject to the existing TaskFlow User.
// 3. Only make it required later if every application user is guaranteed to be linked.reference/TaskFlow_Authorization_Snapshot/apps/api/src/authorization/authorization.module.ts
import { Module } from '@nestjs/common'
import { PrismaModule } from '../prisma/prisma.module'
import { AuthorizationService } from './authorization.service'
import { WorkspacePermissionsGuard } from './workspace-permissions.guard'
@Module({
imports: [PrismaModule],
providers: [AuthorizationService, WorkspacePermissionsGuard],
exports: [AuthorizationService, WorkspacePermissionsGuard],
})
export class AuthorizationModule {}reference/TaskFlow_Authorization_Snapshot/apps/api/src/authorization/authorization.service.ts
import { Injectable } from '@nestjs/common'
import type { MembershipRole } from '../generated/prisma/client'
import { PrismaService } from '../prisma/prisma.service'
import type { AuthenticatedUser } from '../auth/authenticated-user'
import { Permission } from './permissions'
import { ROLE_PERMISSIONS } from './role-permissions'
import {
ApplicationUserNotLinkedException,
PermissionDeniedException,
WorkspaceAccessDeniedException,
AuthorizedTaskNotFoundException,
} from '../common/errors/authorization.exceptions'
@Injectable()
export class AuthorizationService {
constructor(private readonly prisma: PrismaService) {}
async requireApplicationUser(auth: AuthenticatedUser) {
const user = await this.prisma.user.findUnique({
where: { authSubject: auth.id },reference/TaskFlow_Authorization_Snapshot/apps/api/src/authorization/permissions.ts
export enum Permission {
WORKSPACE_READ = 'workspace.read',
WORKSPACE_MANAGE = 'workspace.manage',
MEMBER_MANAGE = 'member.manage',
PROJECT_CREATE = 'project.create',
PROJECT_UPDATE = 'project.update',
TASK_READ = 'task.read',
TASK_CREATE = 'task.create',
TASK_UPDATE_ANY = 'task.update.any',
TASK_UPDATE_ASSIGNED = 'task.update.assigned',
TASK_DELETE = 'task.delete',
}reference/TaskFlow_Authorization_Snapshot/apps/api/src/authorization/role-permissions.ts
import { MembershipRole } from '../generated/prisma/client'
import { Permission } from './permissions'
export const ROLE_PERMISSIONS: Record<MembershipRole, ReadonlySet<Permission>> = {
OWNER: new Set(Object.values(Permission)),
ADMIN: new Set([
Permission.WORKSPACE_READ,
Permission.MEMBER_MANAGE,
Permission.PROJECT_CREATE,
Permission.PROJECT_UPDATE,
Permission.TASK_READ,
Permission.TASK_CREATE,
Permission.TASK_UPDATE_ANY,
Permission.TASK_DELETE,
]),
MEMBER: new Set([
Permission.WORKSPACE_READ,
Permission.TASK_READ,
Permission.TASK_CREATE,
Permission.TASK_UPDATE_ASSIGNED,reference/TaskFlow_Authorization_Snapshot/apps/api/src/authorization/workspace-permissions.decorator.ts
import { Reflector } from '@nestjs/core'
import type { Permission } from './permissions'
export const RequireWorkspacePermissions = Reflector.createDecorator<Permission[]>()reference/TaskFlow_Authorization_Snapshot/apps/api/src/authorization/workspace-permissions.guard.ts
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import type { FastifyRequest } from 'fastify'
import type { AuthenticatedUser } from '../auth/authenticated-user'
import { AuthorizationService } from './authorization.service'
import { RequireWorkspacePermissions } from './workspace-permissions.decorator'
@Injectable()
export class WorkspacePermissionsGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly authorization: AuthorizationService,
) {}
async canActivate(context: ExecutionContext) {
const required = this.reflector.getAllAndOverride(RequireWorkspacePermissions, [
context.getHandler(),
context.getClass(),
])
if (!required?.length) {reference/TaskFlow_Authorization_Snapshot/apps/api/src/common/errors/authorization.exceptions.ts
import { ForbiddenException, NotFoundException } from '@nestjs/common'
export class ApplicationUserNotLinkedException extends ForbiddenException {
constructor() {
super({
code: 'APPLICATION_USER_NOT_LINKED',
message: 'The authenticated identity is not linked to a TaskFlow user.',
})
}
}
export class WorkspaceAccessDeniedException extends ForbiddenException {
constructor() {
super({ code: 'WORKSPACE_ACCESS_DENIED', message: 'You do not have access to this workspace.' })
}
}
export class PermissionDeniedException extends ForbiddenException {
constructor(permission: string) {
super({ code: 'PERMISSION_DENIED', message: `Permission ${permission} is required.` })reference/TaskFlow_Authorization_Snapshot/apps/api/src/tasks/tasks.service.authorization-example.ts
import { ForbiddenException, Injectable } from '@nestjs/common'
import type { AuthenticatedUser } from '../auth/authenticated-user'
import { AuthorizationService } from '../authorization/authorization.service'
import { Permission } from '../authorization/permissions'
import { PrismaService } from '../prisma/prisma.service'
@Injectable()
export class TasksService {
constructor(
private readonly prisma: PrismaService,
private readonly authorization: AuthorizationService,
) {}
async findOneForUser(auth: AuthenticatedUser, taskId: string) {
const ctx = await this.authorization.requireTaskContext(auth, taskId)
this.authorization.assertPermission(ctx.membership.role, Permission.TASK_READ)
return this.prisma.task.findUniqueOrThrow({ where: { id: taskId } })
}
async updateForUser(auth: AuthenticatedUser, taskId: string, input: unknown) {reference/TaskFlow_Authorization_Snapshot/apps/api/src/workspaces/workspaces.controller.permissions-example.ts
import { Controller, Param, Post, UseGuards } from '@nestjs/common'
import { SupabaseAuthGuard } from '../auth/supabase-auth.guard'
import { WorkspacePermissionsGuard } from '../authorization/workspace-permissions.guard'
import { RequireWorkspacePermissions } from '../authorization/workspace-permissions.decorator'
import { Permission } from '../authorization/permissions'
@Controller('workspaces/:workspaceId/projects')
@UseGuards(SupabaseAuthGuard, WorkspacePermissionsGuard)
export class WorkspaceProjectsController {
@Post()
@RequireWorkspacePermissions([Permission.PROJECT_CREATE])
create(@Param('workspaceId') workspaceId: string) {
// Delegate to a Project service. The guard has already verified
// authenticated membership + coarse workspace permission.
return { workspaceId }
}
}reference/TaskFlow_Authorization_Snapshot/tests/authorization-smoke-tests.ps1
$api = 'http://127.0.0.1:4000'
$token = Read-Host 'Paste a current DEVELOPMENT access token'
$headers = @{ Authorization = "Bearer $token" }
Write-Host '1) No token -> 401'
try { Invoke-RestMethod "$api/tasks" } catch { $_.Exception.Response.StatusCode.value__ }
Write-Host '2) Authenticated but unrelated workspace -> 403 or scoped 404 depending endpoint policy'
# Replace with a workspace/task outside the authenticated user's membership.
Write-Host '3) Member can read workspace-scoped task -> 200'
# Invoke-RestMethod "$api/tasks/<member-visible-task-id>" -Headers $headers
Write-Host '4) Member updates assigned task -> success'
# Invoke-RestMethod ... -Method Patch -Headers $headers -ContentType 'application/json'
Write-Host '5) Member updates another member task -> 403'
Write-Host '6) Member deletes task -> 403'