# Osolix · RBAC Test Matrix — v1

**Status:** Wave-B Foundation evidence · 2026-Q2
**Owner:** CISO
**Audit dimensions:** #11 Security · #12 Multi-tenant isolation
**Re-attestation:** On every role-permission change + quarterly

This matrix enumerates the role × resource × action combinations the platform must enforce. Each row is verified by an automated test (`tests/integration/rbac/*.spec.ts`) plus a manual quarterly walkthrough.

## System roles

| Role | Scope | Description |
|---|---|---|
| **SystemOwner** | Cross-tenant | Anas — the immutable founder record; bypasses all gates |
| **OsolixAdmin** | Cross-tenant | Vendor-side platform admin; bypasses entitlement gate but not data scope |
| **OsolixStaff** | Cross-tenant | Vendor-side support / sales / audit / demo lens |
| **TenantAdmin** | Single tenant | Customer's own admin — full module access for their tenant |
| **SystemAdmin** | Single tenant | Customer's IT admin — settings + integrations |
| **Editor** | Module-scoped | Module-level write access (e.g. "Maintenance Editor") |
| **Viewer** | Module-scoped | Module-level read-only |
| **Custodian** | Asset-scoped | Mobile field operator — only sees assets they're custodian for |
| **Approver** | Workflow-scoped | DoA approver for one workflow tier |
| **Anonymous** | Public | Login / signup / marketing — no tenant context |

## Module entitlement gate

| Status | Action |
|---|---|
| **Active / Trial** | Pass |
| **PastDue** | Read-only with banner |
| **Suspended** | 402 Payment Required |
| **Cancelled** | 402 Payment Required (after grace) |
| **Bypass: SystemOwner / OsolixStaff / TenantAdmin / OsolixAdmin** | Always pass (Wave-N audit fix) |

## Critical access tests (every release runs these)

### Tenant isolation probes

| # | Probe | Expectation |
|---|---|---|
| T1 | User A (Tenant 1) calls `GET /api/assets/{id}` with an asset ID owned by Tenant 2 | 404 Not Found (filtered, not 403, to prevent enumeration) |
| T2 | User A passes a `X-Tenant-Id` header for Tenant 2 | Header ignored — tenant resolved from JWT claims |
| T3 | Admin of Tenant 1 invites a user with email matching an existing Tenant 2 user | New AppUser row in Tenant 1 only; emails are not globally unique |
| T4 | SQL injection attempt on a list filter (`?status=Active'; DROP TABLE Assets;--`) | Query parameterised; injection ineffective; logged + alerted |
| T5 | A `GET /api/assets` from a tenant whose `TenantModuleSubscription` for `asset-management` is Suspended | 402 with upgrade URL |

### Role × action grid (sample — full grid in `rbac.spec.ts`)

| Role | List assets | Create asset | Edit asset | Delete asset | Import bulk | Export | Approve transfer | Approve retirement | View financials | Edit master data |
|---|---|---|---|---|---|---|---|---|---|---|
| SystemOwner | ✅ all tenants | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| OsolixAdmin | ✅ all tenants | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| TenantAdmin | ✅ own tenant | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (per matrix) | ✅ (per matrix) | ✅ | ✅ |
| Editor (asset-mgmt) | ✅ own tenant | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | per UserFarColumnAccess | ❌ |
| Viewer (asset-mgmt) | ✅ own tenant | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | per UserFarColumnAccess | ❌ |
| Custodian | ✅ assigned only | ❌ | partial (their assets, limited fields) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Approver | depends | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
| Anonymous | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 | ❌ 401 |

### Field-level access (Financial Data column group)

The `UserFarColumnAccess` table whitelists which `FarColumnGroup` values a user sees. The two sensitive groups:

* **FinancialData** — Cost, NBV, AccumDep, MonthlyDep, Salvage, etc.
* **PersonalData** — Custodian PII fields (national ID, DoB, phone).

Users without FinancialData group see those columns redacted in the FAR + AI-chat answers; the API returns `hasFinancialAccess: false` in the chat envelope so the UI can show the lock-pill.

### Cross-module access

* `[RequireModule(ModuleKeys.AssetManagement)]` — required on `AssetsController`, `TransfersController`, `RetirementsController`, `VerificationsController`, `TagsController`.
* `[RequireModule(ModuleKeys.Maintenance)]` — required on `MaintenanceController`, `WorkOrdersController`.
* `[RequireModule(ModuleKeys.Inventory)]` — required on `InventoryController`, `PartsController`.
* `[RequireModule(ModuleKeys.Fleet)]` — required on `FleetController`.
* `[RequireModule(ModuleKeys.Lease)]` — required on `LeasesController`.
* `[RequireModule(ModuleKeys.AssetBudget)]` — required on `CapexController`.
* `[RequireModule(ModuleKeys.Cwip)]` — required on `CwipController`.

A controller without `[RequireModule]` is platform-wide (Dashboard, Reports, Admin). The audit gate verifies the attribute is present on every solution-scoped controller.

## Open items
* Generate the full role × endpoint matrix automatically by reflecting over controller attributes (CI step).
* Add probe T1–T5 as Playwright e2e tests that run on every PR.
