PSA/context.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

146 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Code Context: Non-RBAC (ABAC-candidate) Constraints Across Product Areas
## Files Retrieved
1. `server/src/interfaces/authorization.interface.ts` (full) — ABAC scaffold: `IPolicy`, `ICondition` (userAttribute/operator/resourceAttribute) already defined but not wired into enforcement
2. `packages/auth/src/lib/policy/PolicyEngine.ts` (full) — ABAC policy engine: evaluates conditions (==, !=, contains) comparing user attributes vs resource attributes
3. `packages/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)
4. `packages/auth/src/lib/attributes/AttributeSystem.ts` (full) — Attribute base classes: DBFieldAttribute, ComputedAttribute, StaticAttribute
5. `packages/auth/src/actions/policyActions.ts` (full) — CRUD for policies, getUserAttributes, getTicketAttributes, evaluateAccess — the ABAC wiring surface
6. `packages/auth/src/lib/withAuth.ts` (full) — Session auth wrapper; injects user + tenant context
7. `server/src/lib/auth/rbac.ts` (full) — Core RBAC: `hasPermission()` checks role→permission with msp/client flag gating
8. `server/src/middleware.ts` (full) — Edge middleware: API key gate, user_type routing (internal→/msp, client→/client-portal)
9. `packages/tickets/src/lib/clientPortalVisibility.ts` (full) — **Board-level visibility groups**: `getClientContactVisibilityContext()` + `applyVisibilityBoardFilter()` — a concrete ABAC pattern
10. `packages/client-portal/src/actions/client-portal-actions/client-tickets.ts` (lines 1220) — **Client→own client_id + visibility group board filter** on ticket queries
11. `packages/client-portal/src/actions/client-portal-actions/visibilityGroupActions.ts` (lines ~130165) — **is_client_admin** attribute gate for visibility group management
12. `packages/client-portal/src/lib/clientAuth.ts` (full) — `getAuthenticatedClientId()`: user→contact→client_id ownership chain
13. `packages/client-portal/src/actions/client-portal-actions/client-documents.ts` (lines 180) — Client document access gated by resolved client_id
14. `server/src/app/api/documents/view/[fileId]/route.ts` (lines 100400) — **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 access
15. `packages/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 delegation
16. `packages/scheduling/src/actions/timeSheetActions.ts` (lines 110200) — **Timesheet approval scoping**: non-read_all users see only team members where they are manager_id; reports-to subordinates via teams-v2 flag
17. `packages/billing/src/actions/quoteActions.ts` (lines 720850) — **Quote approval workflow**: status gates (draft→pending_approval→approved), separate `requireQuoteApprovePermission()`
18. `packages/billing/src/actions/recurringApprovalBlockers.ts` (lines 160) — **Billing blocked by time approval status**: invoice generation checks `time_entries.approval_status` != 'APPROVED'
19. `packages/projects/src/actions/projectTaskCommentActions.ts` (lines 145175) — **Comment edit: own comment OR internal user** — attribute check on user_id match + user_type
20. `packages/tags/src/lib/permissions.ts` (full) — Duplicated RBAC with msp/client flag — candidate for ABAC consolidation
21. `server/src/lib/extensions/gateway/auth.ts` (full) — Extension proxy resolves user_type + client_id for runner forwarding; `assertAccess()` is a TODO stub
22. `packages/client-portal/src/actions/client-portal-actions/client-billing-metrics.ts` (lines 180) — Billing metrics scoped to user's client_id via contact chain
23. `server/src/lib/api/controllers/ApiBaseController.ts` (lines 1130) — API key auth + `checkPermission()` — pure RBAC, no ABAC
24. `server/src/app/api/v1/tickets/[id]/route.ts` area + `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`**
```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`**
```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`**
```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 120330)
Checks in order:
- `isTenantLogo` → public
- `user.user_type === 'internal'` → full access
- `associatedUserId === user.user_id` → own avatar
- `associatedContactId === user.contact_id` → own contact avatar
- `userClientId === associatedClientId` + `is_client_visible` → client doc
- `associatedUserId && same tenant` → same-tenant avatar
- team association + same tenant
- `project_task → project.client_id === userClientId` + `is_client_visible`
- `contract → billing_plans.company_id === userClientId` + `is_client_visible`
- `ticket → contact_name_id match OR client_id match` + `is_client_visible`
---
## Architecture
### Current Access Control Layers
1. **Edge Middleware** (`server/src/middleware.ts`): API key presence check, user_type routing (internal vs client)
2. **RBAC** (`packages/auth/src/lib/rbac.ts` + `server/src/lib/auth/rbac.ts`): `hasPermission(user, resource, action)` — role-based with msp/client flag gating
3. **ABAC Scaffold** (`packages/auth/src/lib/policy/PolicyEngine.ts`): PolicyEngine + EntityAttributes exist but `evaluateAccess()` is not called anywhere in production code
4. **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:
1. It's not called anywhere in production request paths
2. EntityAttributes only cover User and Ticket (need Client, Project, Document, Invoice, TimeEntry, Contract, etc.)
3. The `evaluateAccess()` method matches on `resource.constructor.name` which 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.