-
Notifications
You must be signed in to change notification settings - Fork 3
Access Control
Access control in NatSuite happens at two levels:
- Application level: JWT authentication and role based guards enforce access at the API level
- Database level: RLS policies enforce row level access control using JWT claims injected into PostgreSQL sessions
JwtAuthGuard (backend/src/auth/jwtauth.guard.ts, backend-cm/src/auth/jwtauth.guard.ts)
Validates JWT tokens for all incoming requests. The @Public() decorator can be used to bypass authentication.
Process:
- Intercepts all requests before they reach controllers/resolvers
- Validates JWT token signature and expiration using JWKS
- Extracts user claims (idir_user_guid, client_roles, exp) and attaches them to
request.user - Throws
UnauthorizedExceptionif token is invalid or missing (unless endpoint is@Public())
JwtRoleGuard (backend/src/auth/jwtrole.guard.ts, backend-cm/src/auth/jwtrole.guard.ts)
Enforces role-based access control at the API level. Endpoints / functions are decorated with @Roles() to specify required roles.
Process:
- Checks if endpoint is marked
@Public()(bypasses authorization) - Compares user's
client_rolesfrom JWT against required from@Roles()decorator on the endpoint - Grants access if user has at least one required role
Both backend and backend-cm use a similar approach to inject JWT claims into PostgreSQL session variables used to evaluate RLS policies. The key difference is the ORM used: backend uses TypeORM, backend-cm uses Prisma.
Intercepts database queries at the TypeORM DataSource level using a global module that wraps read operations in transactions with JWT claims from the request set as transaction scoped local variables.
Components:
- RequestInterceptor: Stores incoming requests (with JWT user data) in AsyncLocalStorage for request-scoped access
- PgSessionModule: On module initialization, wraps the various query methods to intercept read operations (SELECT, WITH, etc.) and wrap them in a transaction with the JWT claims from the request.user object stored in AsyncLocalStorage
Process:
- RequestInterceptor captures each request and stores it in AsyncLocalStorage
- PgSessionModule overrides
DataSource.query(),EntityManager.query(), andQueryRunner.query()methods - For read operations, creates a transaction and sets session variables:
jwt.claims.idir_user_guid,jwt.claims.client_roles,jwt.claims.exp - Executes the original query within the transaction, allowing RLS policies to access the JWT claims
- Commits the transaction after query execution
Uses custom Prisma extensions to intercept queries and wrap read operations in transactions with JWT claims from the request.
Components:
- RequestInterceptor: Stores incoming requests (supports both HTTP and GraphQL contexts) in AsyncLocalStorage
-
Prisma Extension (
createPgSessionExtension): Factory function that returns a Prisma extension wrapping all model operations that are read requests (findUnique, findMany, etc.) and wraps them in a transaction with the JWT claims from the request.user object stored in AsyncLocalStorage
Process:
- RequestInterceptor captures requests and stores them in AsyncLocalStorage
- Prisma extension intercepts
$allModels.$allOperationsfor read operations (findUnique, findMany, count, aggregate, etc.) - For read operations, creates a transaction and sets the same session variables:
jwt.claims.idir_user_guid,jwt.claims.client_roles,jwt.claims.exp - Executes the original query within the transaction context
- Commits the transaction after query execution
Business rules that dictate access control rules can be implemented as RLS policies in the database. The data needed to evaluate the RLS policies is distributed between the schemas, which requires granting select privileges to the roles used by other schemas.
RLS policies use the session variables set by the backends:
-
jwt.claims.idir_user_guid: User's IDIR GUID for identity checks -
jwt.claims.client_roles: Comma-separated list of user roles for authorization -
jwt.claims.exp: Token expiration timestamp for validation
Policies evaluate these variables using current_setting('jwt.claims.*', true) to enforce access control at the database level, ensuring users can only access rows they're authorized to view based on their roles and id.
NOTE: the policies include cross-schema access which requires selective grants on the postgres roles. Since the tables involved in the grants need to exist before the grant can take place, the policies and necessary grants have all been placed in the complaint migrations as they run last.