Module: Permissions & FGA
Relevant source files
The following files were used as context for generating this wiki page:
The Permissions & FGA module provides a Relationship-Based Access Control (ReBAC) system using OpenFGA. It manages the lifecycle of authorization models, relationship tuples, and permission checks to control access to various "tables" (resources) within the application. The system integrates a Node.js backend with an OpenFGA server to enforce granular CRUD permissions based on user roles and direct associations.
The module serves as the central authority for determining whether a user can create, read, update, or delete records in specific modules. It enables a "Permission Matrix" where administrators can toggle access for different roles, which then dynamically updates the user interface and protects backend API endpoints.
Sources: PLAN.md:3-8, backend/src/fga/fgaClient.ts:94-110
Authorization Model & Schema
The system uses a domain-specific language (DSL) to define relationships between users, roles, and tables. The authorization model is defined in infra/openfga/model.fga and contains three primary types: user, role, and table.
Model Structure
Note: The diagram illustrates the relationship-based hierarchy where table permissions are derived from assigned badges or role membership.
OpenFGA Client Integration
The fgaClient.ts utility acts as a wrapper for the @openfga/sdk. It handles connection configuration, model initialization, and provides high-level functions for tuple management and permission verification.
Key Integration Functions
| Function | Purpose | Logic |
|---|---|---|
checkPermission | Validates user access | Checks if user:userId has can_action on table:tableSlug |
writeTuples | Atomic updates | Batch writes or deletes relationship tuples with Ignore flags for duplicates/missing |
assignRoleToUser | Role membership | Writes a tuple: user:userId is member of role:roleName |
grantCrudBadgeToRole | Resource access | Connects role:roleName#member to a specific table relation (e.g., reader) |
Sources: backend/src/fga/fgaClient.ts:94-108, backend/src/fga/fgaClient.ts:162-184, backend/src/fga/fgaClient.ts:205-215
Model Auto-Resolution
The client includes logic to ensure a valid authorization model exists. If the OPENFGA_AUTH_MODEL_ID is not provided or invalid, it reads the local .fga file, transforms it to JSON using @openfga/syntax-transformer, and uploads it to the OpenFGA store.
Sources: backend/src/fga/fgaClient.ts:33-87
Permission Matrix & Admin API
The permissions.routes.ts module provides endpoints for Super Admins to view and modify the global permission state.
Matrix Retrieval Flow
The /matrix endpoint aggregates data from the Postgres database (roles and tables) and performs parallelized OpenFGA checks to build a comprehensive access grid.
Note: This sequence illustrates the heavy-read operation used to populate the administrative UI grid.
Sources: backend/src/modules/permissions/permissions.routes.ts:11-40
Updating Permissions
Permissions are updated via the /update or /up endpoints. These endpoints convert boolean flags from the request body into OpenFGA tuple operations.
- Allowed: true → Added to the
writesarray. - Allowed: false → Added to the
deletesarray.
Sources: backend/src/modules/permissions/permissions.routes.ts:43-85
Middleware & Access Enforcement
Access control is enforced at the route level using specialized middleware and "Visualization Endpoints" that provide the frontend with the current user's effective permissions.
Authorization Middleware
The authorizeTable middleware prevents unauthorized access to protected resource routes by performing a checkPermission call before reaching the controller.
// backend/src/middleware/authorizeTable.ts:6-18
export const authorizeTable = (action: FgaAction, tableSlug: string) => {
return async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: "Unauthorized" });
const allowed = await checkPermission(req.user.id, tableSlug, action);
if (!allowed) return res.status(403).json({ error: "Forbidden" });
return next();
};
};
Visualization Endpoints (/me)
GET /me/allowed-tables: Returns a list of all tables where the user has at leastcan_readpermission. Used to populate the sidebar.GET /me/table-permissions: Returns a detailed CRUD boolean object for a specific table. Used to show/hide UI action buttons.
Sources: backend/src/modules/me/me.routes.ts:11-57, backend/src/middleware/authorizeTable.ts:6-18
Data Relationships Summary
| Entity | OpenFGA Object | Relation | Subject |
|---|---|---|---|
| Role Assignment | role:<name> | member | user:<id> |
| Role Permission | table:<slug> | reader, creator, etc. | role:<name>#member |
| Direct User Access | table:<slug> | reader, creator, etc. | user:<id> |
Sources: PLAN.md:85-110, backend/src/fga/fgaClient.ts:141-159
Conclusion
The Permissions & FGA module centralizes authorization logic, moving it out of the application code and into a relationship-based model. By combining OpenFGA's tuple-based checks with Express middleware, the system ensures that both the user interface and the underlying API endpoints consistently respect the configured permission matrix. This architecture allows for real-time permission updates without requiring code changes or database migrations.