Course navigationPhase 19
Volume 05 · Phase 19

Authorization, Ownership, RBAC & Permissions

Enforce resource ownership, workspace membership, roles, and permissions on the API with server-side policies.

16lessons + labs
4klesson words
14source references
What you will ship

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.

Figure 19.1 — Where this phase lives in TaskFlow
verified identity
resource lookup
membership
role/permission policy
allowed/denied use case
audit/error
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

Enforce ownership and role checks on the server even when the UI hides forbidden actions.

02

Model permissions around actions and resources instead of scattering role-name comparisons everywhere.

03

Check workspace membership before allowing resource operations tied to that workspace.

04

Use least privilege: a role should receive only the capabilities its workflows require.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    List TaskFlow actions such as workspace.read, task.create, task.update, member.manage.

  2. 02

    Map roles to capabilities without scattering role-name if statements across controllers.

  3. 03

    Load membership for the relevant workspace and treat it as authorization evidence.

  4. 04

    Enforce owner/member/role checks in server-side policy code.

  5. 05

    Decide when 403 reveals too much and when a 404-style response better protects resource existence.

  6. 06

    Write a matrix test covering at least owner, member, outsider, unauthenticated caller, and missing resource.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

Authorization only in frontend

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 2

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.

Failure drill 3

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.

Failure drill 4

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.

Watch-order style

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.

01

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

2 min
02

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

2 min
03

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 &

2 min
04

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 &

2 min
05

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

2 min
06

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

2 min
07

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

2 min
08

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

2 min
09

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

2 min
10

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

2 min
11

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

2 min
12

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

2 min
LAB

Guided Build Lab

Purpose Enforce resource ownership, workspace membership, roles, and permissions on the API with server-side policies.

3 min
LAB

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.

2 min
LAB

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?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 19 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

List TaskFlow actions such as workspace.read, task.create, task.update, member.manage.

Practice

Load membership for the relevant workspace and treat it as authorization evidence.

Debug

reproduce and diagnose — Authorization only in frontend.

Independent

deliver Permission model + guards/policies protecting TaskFlow workspace/task actions with deliberate 401/403/404 semantics.

Explain-back

Can model permission checks by action/resource and Can test authorization matrixes.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 19's engineering goal?
02Which practice best matches Phase 19's engineering goal?
03Which practice best matches Phase 19's engineering goal?
04Which practice best matches Phase 19's engineering goal?
05You are working through the Phase 19 lab. Which next action is the most disciplined step?
06You are working through the Phase 19 lab. Which next action is the most disciplined step?
07You are working through the Phase 19 lab. Which next action is the most disciplined step?
08You are working through the Phase 19 lab. Which next action is the most disciplined step?
Source snapshot

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'