ProficientNowTechRFCs

RFC: Authorization Layer Overhaul with CASL

RFC: Authorization Layer Overhaul with CASL

Abstract

This RFC proposes a complete overhaul of the current authorization layer in the PNow ATS v2 application. The objective is to replace the existing temporary solution with a robust, scalable, and user-customizable Policy-Based Access Control (PBAC) / Attribute-Based Access Control (ABAC) system.

After evaluating options, this proposal recommends adopting CASL (Code Access Security Library) over Cerbos. CASL's isomorphic nature, deep integration with Prisma (our ORM), and ability to handle dynamic, database-stored rules make it the superior choice for our specific requirement of allowing end-users to customize permission policies.

Motivation

The current authorization implementation is a "hot fix" that is rigid, difficult to maintain, and does not function as intended. It fails to meet the core business requirement: User-Defined Policy Customization.

We need a system that supports:

  1. Granular Permissions: Control access not just by role, but by specific attributes (e.g., "Department Lead can only edit Candidates in their own department").
  2. User Customizability: Tenants/Admins must be able to define and modify access policies through the UI, without developer intervention or code deployment.
  3. Performance: Authorization logic must apply at the database query level to prevent over-fetching sensitive data.
  4. Consistency: Access rules must be consistent across the Backend (API security) and Frontend (UI visibility).

Approaches

We evaluated two primary candidates for the authorization engine: Cerbos and CASL.

Option 1: Cerbos

Cerbos is a self-hosted, open-source Policy Decision Point (PDP) that runs as a separate service (sidecar or standalone).

  • Pros:
    • Decouples authorization logic entirely from application code.
    • Language-agnostic (great for polyglot microservices).
    • "GitOps" friendly: Policies are typically defined in YAML and version controlled.
  • Cons:
    • Infrastructure Overhead: Requires deploying and maintaining a separate service.
    • Dynamic Policy Friction: While Cerbos supports dynamic loading, it is optimized for static policies managed by developers/ops. Bridging the gap between a "User UI" for permissions and Cerbos YAML policies would require building a complex translation and storage layer.
    • Database Integration: While it has query plan adapters, integrating them into our NestJS/Prisma stack adds another layer of complexity compared to a native library.

CASL is an isomorphic authorization JavaScript/TypeScript library.

  • Pros:
    • Native TypeScript Integration: Fits perfectly into our NestJS (Backend) + Next.js (Frontend) monorepo.
    • Dynamic by Design: CASL is built to hydrate Ability instances from JSON objects. This aligns perfectly with our database schema where GroupPolicy has a configuration JSON field.
    • Prisma Integration: The @casl/prisma package allows us to convert permission rules directly into Prisma where clauses. This enables "Safe Queries" (e.g., prisma.candidate.findMany({ where: accessibleBy(ability).Candidate })), ensuring we never fetch unauthorized data.
    • Isomorphic: We can share the exact same subject definitions and rule logic between backend and frontend.
    • No Infrastructure Overhead: Runs in-process as a standard library.
  • Cons:
    • Tightly coupled to the Node.js ecosystem (not an issue for us as we are fully Node/TS).

Proposal

We propose implementing the authorization layer using CASL. This approach leverages our existing architecture and meets the requirement for user-customizable policies.

Core Architecture

  1. Policy Storage:

    • We will utilize the existing GroupPolicy model in our Prisma schema.
    • The configuration field (Json) will store CASL-compatible rules (e.g., [{ "action": "read", "subject": "Candidate", "conditions": { "ownerId": "${user.id}" } }]).
    • Tenants can create/edit these JSON rules via a UI Builder, effectively creating their own policies.
  2. Backend Implementation (NestJS):

    • Factory: A CaslAbilityFactory will be created. On every request, it will:
      1. Fetch the user's UserGroup and related GroupPolicy records.
      2. Merge the raw JSON rules.
      3. Interpolate variables (e.g., replace ${user.id} with the actual ID).
      4. Return a user-specific Ability instance.
    • Guards: A global PoliciesGuard will intercept requests and check permissions using the Ability.
    • Decorators: @CheckPolicies() decorators will be used on Controllers to define required permissions.
    • Data Access: We will use @casl/prisma in our Services to automatically filter database queries based on the user's ability.
  3. Frontend Implementation (Next.js):

    • Synchronization: On login/bootstrap, the backend will send the computed list of JSON rules to the frontend.
    • Context: A React Context (AbilityContext) will hold the CASL Ability instance.
    • Components: We will use CASL's <Can> component (or a custom wrapper) to conditionally render UI elements (e.g., hiding the "Delete" button if the user lacks permission).

Features to be Implemented

  1. Dynamic Policy Engine:
    • System capability to load permissions from the database at runtime.
    • Support for "Standard/System" policies (seeded) and "Custom" policies (user-created).
  2. Visual Policy Builder:
    • A frontend UI that allows admins to construct policies using dropdowns (Select Subject -> Select Action -> Add Conditions), which saves as valid CASL JSON.
  3. Secure Querying:
    • Implementation of accessibleBy in Repositories/Services to ensure no data leaks occur at the DB level.
  4. Field-Level Security:
    • Utilize CASL's field limitations to prevent users from reading/updating sensitive fields (e.g., salary) even if they have access to the resource.
  5. Multi-Tenancy Isolation:
    • Ensure all policy evaluations are strictly scoped to the user's tenant_id.

Why CASL wins for PNow ATS v2

FeatureCerbosCASLVerdict
User-Customizable RulesComplex (Requires translation layer)Native (JSON storage)CASL
PerformanceNetwork hop requiredIn-memoryCASL
DB IntegrationQuery Plan Adapter@casl/prisma NativeCASL
Frontend ReuseSeparate logic neededIsomorphic (Shared code)CASL
MaintenanceHigh (New Service)Low (Library Update)CASL

By choosing CASL, we align with our stack (NestJS/Prisma/Next.js), simplify our deployment, and directly address the "user-customizable" requirement by storing policies as data, not code.

On this page