Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
11 KiB
Code Context: Non-RBAC (ABAC-candidate) Constraints Across Product Areas
Files Retrieved
server/src/interfaces/authorization.interface.ts(full) — ABAC scaffold:IPolicy,ICondition(userAttribute/operator/resourceAttribute) already defined but not wired into enforcementpackages/auth/src/lib/policy/PolicyEngine.ts(full) — ABAC policy engine: evaluates conditions (==, !=, contains) comparing user attributes vs resource attributespackages/auth/src/lib/attributes/EntityAttributes.ts(full) — User attributes (user_id, team_id, roles, isAdmin) and Ticket attributes (creator_id, assignee_id, team_id, status, isOverdue)packages/auth/src/lib/attributes/AttributeSystem.ts(full) — Attribute base classes: DBFieldAttribute, ComputedAttribute, StaticAttributepackages/auth/src/actions/policyActions.ts(full) — CRUD for policies, getUserAttributes, getTicketAttributes, evaluateAccess — the ABAC wiring surfacepackages/auth/src/lib/withAuth.ts(full) — Session auth wrapper; injects user + tenant contextserver/src/lib/auth/rbac.ts(full) — Core RBAC:hasPermission()checks role→permission with msp/client flag gatingserver/src/middleware.ts(full) — Edge middleware: API key gate, user_type routing (internal→/msp, client→/client-portal)packages/tickets/src/lib/clientPortalVisibility.ts(full) — Board-level visibility groups:getClientContactVisibilityContext()+applyVisibilityBoardFilter()— a concrete ABAC patternpackages/client-portal/src/actions/client-portal-actions/client-tickets.ts(lines 1–220) — Client→own client_id + visibility group board filter on ticket queriespackages/client-portal/src/actions/client-portal-actions/visibilityGroupActions.ts(lines ~130–165) — is_client_admin attribute gate for visibility group managementpackages/client-portal/src/lib/clientAuth.ts(full) —getAuthenticatedClientId(): user→contact→client_id ownership chainpackages/client-portal/src/actions/client-portal-actions/client-documents.ts(lines 1–80) — Client document access gated by resolved client_idserver/src/app/api/documents/view/[fileId]/route.ts(lines 100–400) — Rich attribute-based document access: checks user_type, ownership (own avatar, own contact), client association match, project_task→client ownership, contract→client ownership, ticket→contact/client ownership, is_client_visible flag, tenant-logo public access, same-tenant team avatar accesspackages/scheduling/src/actions/timeEntryDelegationAuth.ts(full) —assertCanActOnBehalf(): self / manager-of-subject (team membership + manager_id) / reports-to-chain (teams-v2 flag) / tenant-wide (read_all) — classic ABAC delegationpackages/scheduling/src/actions/timeSheetActions.ts(lines 110–200) — Timesheet approval scoping: non-read_all users see only team members where they are manager_id; reports-to subordinates via teams-v2 flagpackages/billing/src/actions/quoteActions.ts(lines 720–850) — Quote approval workflow: status gates (draft→pending_approval→approved), separaterequireQuoteApprovePermission()packages/billing/src/actions/recurringApprovalBlockers.ts(lines 1–60) — Billing blocked by time approval status: invoice generation checkstime_entries.approval_status!= 'APPROVED'packages/projects/src/actions/projectTaskCommentActions.ts(lines 145–175) — Comment edit: own comment OR internal user — attribute check on user_id match + user_typepackages/tags/src/lib/permissions.ts(full) — Duplicated RBAC with msp/client flag — candidate for ABAC consolidationserver/src/lib/extensions/gateway/auth.ts(full) — Extension proxy resolves user_type + client_id for runner forwarding;assertAccess()is a TODO stubpackages/client-portal/src/actions/client-portal-actions/client-billing-metrics.ts(lines 1–80) — Billing metrics scoped to user's client_id via contact chainserver/src/lib/api/controllers/ApiBaseController.ts(lines 1–130) — API key auth +checkPermission()— pure RBAC, no ABACserver/src/app/api/v1/tickets/[id]/route.tsarea +ApiTicketController.ts(full) — Ticket API: pure RBAC (ticket:read/update/delete), no board/client/visibility filtering in API layer
Key Code
1. Existing ABAC Scaffold (unwired)
packages/auth/src/lib/policy/PolicyEngine.ts
evaluateAccess(user: IUserWithRoles, resource: any, action: string): boolean {
for (const policy of this.policies) {
if (policy.resource === resource.constructor.name && policy.action === action) {
if (this.evaluateConditions(user, resource, policy.conditions)) return true;
}
}
return false;
}
packages/auth/src/lib/attributes/EntityAttributes.ts — Only User and Ticket entity attributes defined. Missing: Client, Project, Document, Invoice, TimeEntry, Contract, Schedule, Integration entities.
2. Client Portal Visibility Groups (ABAC in practice)
packages/tickets/src/lib/clientPortalVisibility.ts
export interface ContactVisibilityContext {
contactId: string;
clientId: string;
visibilityGroupId: string | null;
visibleBoardIds: string[] | null; // null = unrestricted
}
export function applyVisibilityBoardFilter(query, visibleBoardIds, boardColumn = 't.board_id') {
if (visibleBoardIds === null) return query; // unrestricted
if (visibleBoardIds.length === 0) { query.whereRaw('1 = 0'); return query; }
query.whereIn(boardColumn, visibleBoardIds);
return query;
}
3. Time Entry Delegation Auth (manager-chain ABAC)
packages/scheduling/src/actions/timeEntryDelegationAuth.ts
export async function assertCanActOnBehalf(actor, tenant, subjectUserId, db): Promise<DelegationScope> {
if (actor.user_id === subjectUserId) return 'self';
const canApprove = await hasPermission(actor, 'timesheet', 'approve', db);
if (!canApprove) throw new Error('Permission denied');
const canReadAll = await hasPermission(actor, 'timesheet', 'read_all', db);
if (canReadAll) return 'tenant-wide';
if (await isManagerOfSubject(db, tenant, actor.user_id, subjectUserId)) return 'manager';
if (reportsToEnabled && await User.isInReportsToChain(db, actor.user_id, subjectUserId)) return 'manager';
throw new Error('Permission denied');
}
4. Document View Access (multi-attribute check)
server/src/app/api/documents/view/[fileId]/route.ts (lines 120–330)
Checks in order:
isTenantLogo→ publicuser.user_type === 'internal'→ full accessassociatedUserId === user.user_id→ own avatarassociatedContactId === user.contact_id→ own contact avataruserClientId === associatedClientId+is_client_visible→ client docassociatedUserId && same tenant→ same-tenant avatar- team association + same tenant
project_task → project.client_id === userClientId+is_client_visiblecontract → billing_plans.company_id === userClientId+is_client_visibleticket → contact_name_id match OR client_id match+is_client_visible
Architecture
Current Access Control Layers
- Edge Middleware (
server/src/middleware.ts): API key presence check, user_type routing (internal vs client) - RBAC (
packages/auth/src/lib/rbac.ts+server/src/lib/auth/rbac.ts):hasPermission(user, resource, action)— role-based with msp/client flag gating - ABAC Scaffold (
packages/auth/src/lib/policy/PolicyEngine.ts): PolicyEngine + EntityAttributes exist butevaluateAccess()is not called anywhere in production code - Inline Attribute Checks (scattered): user_type checks, ownership checks, client_id resolution, board visibility filtering, manager-chain checks
Data Flow
Request → Edge Middleware (user_type routing) → Route Handler
→ withAuth() (session → user + tenant context)
→ hasPermission() (RBAC check)
→ Inline attribute checks (non-RBAC constraints)
Key Observation
ABAC constraints are ad-hoc and scattered — each product area implements its own attribute resolution and filtering inline rather than going through the PolicyEngine. The PolicyEngine exists but is dormant.
Product Area ABAC Constraint Summary
| Area | Constraint Type | Where | Pattern |
|---|---|---|---|
| Tickets | Board visibility groups | packages/tickets/src/lib/clientPortalVisibility.ts |
contact→visibility group→board_ids filter |
| Tickets | Client ownership | packages/client-portal/.../client-tickets.ts:resolveVisibleTicket |
client_id match on ticket |
| Tickets | API layer: no ABAC | server/src/lib/api/controllers/ApiTicketController.ts |
Pure RBAC only |
| Billing/Invoices | Client scoping | packages/client-portal/.../client-billing-metrics.ts |
user→contact→client_id filter |
| Billing/Quotes | Approval status gate | packages/billing/src/actions/quoteActions.ts:800 |
status must be 'pending_approval' |
| Billing/Recurring | Time approval blocker | packages/billing/src/actions/recurringApprovalBlockers.ts |
approval_status != 'APPROVED' blocks invoicing |
| Projects/Documents | Comment ownership | packages/projects/.../projectTaskCommentActions.ts:152 |
own comment OR internal user_type |
| Documents | Multi-entity association | server/src/app/api/documents/view/[fileId]/route.ts |
client/contact/project/contract/ticket ownership chain |
| Documents | is_client_visible flag | Same file | Client users need doc.is_client_visible=true |
| Contacts/Clients | Client admin gate | packages/client-portal/.../visibilityGroupActions.ts:140 |
is_client_admin attribute check |
| Scheduling/Time | Manager chain delegation | packages/scheduling/.../timeEntryDelegationAuth.ts |
self / manager / reports-to / tenant-wide |
| Scheduling/Time | Team manager scope | packages/scheduling/.../timeSheetActions.ts:163 |
team_members + manager_id join |
| Workflows | Permission hierarchy | ee/packages/workflows/.../workflow-schedule-v2-actions.ts:133 |
read fallback to view/manage/admin |
| Integrations | TODO stub | server/src/lib/extensions/gateway/auth.ts:assertAccess() |
// TODO: implement RBAC and per-tenant endpoint checks |
Start Here
Open packages/auth/src/lib/policy/PolicyEngine.ts — this is the existing ABAC engine. It has the attribute comparison logic but:
- It's not called anywhere in production request paths
- EntityAttributes only cover User and Ticket (need Client, Project, Document, Invoice, TimeEntry, Contract, etc.)
- The
evaluateAccess()method matches onresource.constructor.namewhich is fragile
The first task is deciding whether to extend this engine or replace it, then mapping the inline constraints listed above into the chosen ABAC model.