# Platform Service Security Design

## Purpose & Audience

This document describes the security architecture of the platform service. It is intended for engineers working on authorization, compliance, or integration with the platform. For the full architecture, see [platform architecture](../architecture/platform.md). For operational security procedures, see the [platform runbooks](../operations/runbooks/).

---

## Design Principles

Seven principles govern every authorization decision in the system.

| #   | Principle                     | Implication                                                                                                                                                                       |
| --- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | **Fail-closed**               | If the permission system cannot determine access, access is denied. The resolver's outer `try/catch` returns DENIED on any exception.                                             |
| 2   | **Deny-wins**                 | An explicit deny override always takes precedence over role-granted permissions, regardless of how many roles grant it. Deny overrides apply to all users including super admins. |
| 3   | **Tenant-scoped everything**  | Every permission-related entity is scoped to a tenant. No shared rows across tenants. Every DB query includes `tenantId`.                                                         |
| 4   | **Append-only audit**         | Audit logs are immutable. No update, no delete, no soft-delete. PII is handled via crypto-shredding (see below).                                                                  |
| 5   | **Separation of concerns**    | Authentication (Grass/Auth0) is separate from authorization (platform service). The platform never validates JWTs directly.                                                       |
| 6   | **Least privilege**           | Users receive only the minimum permissions required. Default is no access.                                                                                                        |
| 7   | **Code documents invariants** | Every security constraint is enforced by named tests (`inv-*`) that reference the design spec.                                                                                    |

---

## Fail-Closed Enforcement

The permission resolver wraps all logic in a top-level `try/catch`. Any unhandled exception results in `DENIED`:

```
async resolve(request: CheckRequest): Promise<CheckResult> {
  try {
    return await resolveInternal(request);
  } catch (err) {
    logger.error({ err, request }, "Permission resolution failed — denying (fail-closed)");
    return denied(0, "Internal error — access denied");
  }
}
```

This is verified by 7 dedicated invariant tests in `inv-fail-closed.test.ts`.

### Failure modes by dependency

| Dependency                      | Failure behavior                                                     | Rationale                                                                    |
| ------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **Redis (permission cache)**    | Fall through to Aurora DB. Never serve stale grants.                 | Security-critical. Latency degrades (5ms to 50ms) but correctness preserved. |
| **Redis (rate limit counters)** | Fail closed -- reject with 429 + `Retry-After: 5`.                   | Cannot fail open -- would allow unlimited throughput.                        |
| **Redis (credit balance)**      | Fall through to DB. Use atomic `UPDATE` for deductions.              | Concurrent increment safety requires DB-level atomicity.                     |
| **Redis (audit buffer)**        | Fall back to structured pino JSON log. Replay from logs on recovery. | Non-blocking. Audit data is never silently dropped.                          |
| **Aurora DB**                   | Deny all. Log failure. Alert via webhook.                            | Most conservative -- no DB means no data to authorize against.               |

---

## Deny-Wins (Step 8 Before Step 9)

The pipeline checks deny overrides at step 8, before super admin status at step 9. This is intentional: a deny override is a security control that cannot be bypassed even by the highest privilege level.

```
Step 8: Deny override active? --> DENIED (even for super admins)
Step 9: Super admin?          --> only reached if no deny override
```

Because step 8 runs before step 9, a deny override prevents the `superAdminGranted` flag from ever being set. There is no code path where `superAdminGranted=true` and a deny override is present simultaneously. This is verified by `inv-deny-wins.test.ts`.

---

## Tenant Isolation

Every tenant-scoped entity includes a `tenantId` column. Every query filters by it. Cross-tenant data access is prevented at multiple layers:

1. **Database queries** -- all `WHERE` clauses include `tenantId`
2. **Role assignments** -- `UserRole` includes a denormalized `tenantId` with a uniqueness constraint. Application logic verifies `Role.tenantId == tenantId`
3. **Cache key namespacing** -- all cache keys include `tenantId` (e.g., `perm:{tenantId}:{userId}:effective`)
4. **Tenant suspension cascade** -- suspending a tenant flushes ALL cache keys for that tenant via prefix scan

Verified by 7 invariant tests in `inv-tenant-scoped.test.ts`.

### Cross-tenant role assignment prevention

The `UserRole` model includes a denormalized `tenantId`:

```
@@id([tenantId, userId, roleId])
```

The application verifies two conditions before assignment:

1. `Role.tenantId == tenantId` (role belongs to this tenant)
2. A `TenantUser` exists for `(userId, tenantId)` with `status=active`

---

## Crypto-Shredding (Audit Log PII)

Audit logs contain PII (`userId`, `ipAddress`, `userAgent`). The design declares audit logs immutable. GDPR Article 17 grants data subjects the right to erasure. These directly conflict.

**Resolution:** PII fields in audit logs are encrypted per-user with a user-specific encryption key derived from a master key. Erasure deletes the derived key, rendering PII fields unreadable while preserving the audit trail's structural integrity.

### Write path

```
AuditWriter receives entry with { userId, ipAddress, userAgent }
    |
    v
Derive per-user key: HMAC-SHA256(masterKey, userId)
    |
    v
Encrypt PII fields: AES-256-GCM(userId, key), AES-256-GCM(ipAddress, key)
    |
    v
Write to Redis buffer: { encryptedUserId, userIdHash, encryptedIpAddress, ... }
```

The `userIdHash` (HMAC, non-reversible) enables query filtering without exposing the userId.

### Erasure path (GDPR Article 17)

Deleting a user's encryption key renders all their audit entries' PII fields unreadable. Structural data (`action`, `resource`, `outcome`, `timestamp`) remains intact. Entries show `[redacted]` for PII fields when the key is missing.

### Key management

| Component    | Details                                                                                                                                  |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Master key   | 256-bit, hex-encoded, provided via `AUDIT_MASTER_KEY` env var. In dev, a random key is generated (data not recoverable across restarts). |
| Per-user DEK | `HMAC-SHA256(masterKey, userId)`. Cached in-memory for 30 minutes.                                                                       |
| Encryption   | AES-256-GCM with random 12-byte IV per field. Format: `iv:authTag:ciphertext` (hex).                                                     |

**Invariant:** `inv-audit-crypto-shred` -- "After deleting a user's encryption key, all audit entries for that user return `[redacted]` for PII fields. The structural fields remain readable. The entry count does not change."

**Invariant:** Redis audit buffer NEVER contains plaintext PII. Encryption happens before the `LPUSH`.

---

## Shadow Mode

Shadow mode (`SHADOW_MODE=true`, the default in Phase 1) logs permission checks but does not enforce them. This enables:

1. **Validation** -- compare shadow denials against actual access patterns to verify correctness before enforcement
2. **Gradual rollout** -- enable enforcement per-route or per-tenant after shadow validation
3. **Rollback safety** -- if permission data is misconfigured, users are not locked out

When shadow mode is disabled, `AccessService.Check` returns actual GRANTED/DENIED outcomes and consuming middleware enforces them (403 on DENIED).

---

## Input Validation

Permission names are validated against a strict regex before any DB or cache interaction:

```
/^[a-z][a-z0-9_.]*\.[a-z][a-z0-9_]*$/
```

This prevents:

- SQL injection via permission names
- Cache key injection via malformed strings
- Arbitrary string storage in the permissions system

Maximum length: 255 characters.

---

## Super Admin Levels

Cross-tenant platform access for internal staff. Each level is bounded:

| Level       | Capabilities                                              | Restrictions                                     |
| ----------- | --------------------------------------------------------- | ------------------------------------------------ |
| `full`      | All platform operations                                   | None. Audit logged.                              |
| `read_only` | View any tenant's audit logs, permissions, config         | Cannot modify any data.                          |
| `support`   | View + impersonate any user (audit-logged)                | Cannot modify config, roles, plans, permissions. |
| `product`   | View feature analytics. Impersonate in test tenants only. | Cannot access production tenant data.            |
| `analytics` | Cross-tenant aggregate metrics (no PII)                   | Cannot impersonate. Cannot view audit logs.      |

Super admin access is always audit logged. The `analytics` level must go through an aggregation layer that strips PII.

### Data classification override

Super admin GRANTED does NOT skip step 14 (data classification). If a resource has a classification level, step 14 still runs even for super admins. This prevents full super admins from bypassing RESTRICTED data access controls. Implementation: step 9 sets `superAdminGranted=true` and continues to step 14 instead of returning early. Steps 10-13 are skipped.

---

## Separation of Duty (SoD)

`RoleExclusion` defines pairs of mutually exclusive roles within a tenant. When a role assignment is attempted, the SoD validator checks:

1. Does the user already hold the other role in an exclusion pair?
2. Would the assignment create a cycle in the role hierarchy?

Cycle detection uses a BFS traversal with visited-node tracking. Verified by `inv-role-cycle.test.ts`.

---

## Session and Impersonation Security

### Impersonation tokens

- JWT signed by the platform service encoding: super admin ID, target user ID, target tenant ID
- Maximum duration: 1 hour, non-renewable
- Requests run as the TARGET user (their roles, permissions, shares)
- The `acting_as_user_id` field in audit logs is set to the super admin's ID
- Tenant-bound: using a token against a different tenant returns 403
- Step-up auth requirements on the target user still apply

### SCIM token security

- SHA-256 hash stored, plaintext shown ONCE at creation
- One active token per tenant
- Generating a new token revokes the previous one
- Token prefix (first 8-12 chars) stored for log identification
- Tenant binding: a token for tenant A cannot provision users into tenant B

### API key security

- Write-once, shown once. Only SHA-256 hash stored.
- Key prefix stored for identification.
- Rotation: create new key, migrate, revoke old -- no downtime.

---

## Encryption

| Layer           | Mechanism                                                                                          |
| --------------- | -------------------------------------------------------------------------------------------------- |
| At rest         | Aurora encryption (AES-256). API keys/tokens as SHA-256 hashes. Webhook secrets encrypted via KMS. |
| In transit      | TLS 1.2+ for all connections.                                                                      |
| Audit PII       | AES-256-GCM per-user encryption with HMAC-derived keys.                                            |
| Webhook secrets | `encryptedSecret` stored as `Bytes` (KMS envelope encryption).                                     |

---

## Compliance Standards

The security design supports the following standards:

| Standard      | How addressed                                                                                              |
| ------------- | ---------------------------------------------------------------------------------------------------------- |
| SOC 2 Type II | Audit logging, access controls, encryption, session management, change tracking                            |
| SOX           | SoD constraints, approval workflows, permission change log, immutable audit trail                          |
| GDPR          | Data export, erasure via crypto-shredding, consent tracking (opt-in), lawful basis, data residency pinning |
| CCPA/CPRA     | Right to know, right to delete, right to opt-out. 45-day SLA tracking.                                     |
| NIST RBAC     | Core RBAC + hierarchical + static SoD (INCITS 359 Level 2+)                                                |
| OWASP Top 10  | Input validation, broken access control mitigations, security logging                                      |
