SECURITY_MODEL — reporting-service
Sibling: DATA_MODEL · API_CONTRACTS · APPLICATION_LOGIC · platform anchors: docs/07 Security/Compliance/Tenancy, docs/architecture/ADR-0002 Multi-tenancy
1. Authentication
| Caller | Mechanism |
|---|---|
BFF (bff-backoffice-service, bff-guest-portal-service) | Internal mTLS (workload-identity-bound service accounts) + signed JWT carrying userId, tenantId, propertyAccess[], roles[] |
sync-service → /internal/sync/* | Internal mTLS + service JWT with aud = reporting-service |
Cloud Scheduler → /internal/scheduler/fire | OIDC-signed token, audience pinned, iss = accounts.google.com, service-account allowlist enforced |
| Pub/Sub push subscriptions | OIDC token verified, aud = https://reporting.melmastoon.ghasi.io |
| GCS signed URLs | Service-issued, V4 signature, ≤ 15 min TTL |
| External regulatory adapters | OAuth2 client credentials per jurisdiction; secrets in Secret Manager, fetched via Workload Identity at boot |
End-user JWTs are issued by iam-service (15 min access, 8 h refresh). reporting-service validates JWTs against the platform JWKS and rejects with MELMASTOON.IAM.AUTH_INVALID on failure.
2. Authorization
We use RBAC + ABAC (07 §4). Permission checks are encoded in AuthorizationDecision (see APPLICATION_LOGIC §6). Required permissions:
| Permission | Allows | Default roles |
|---|---|---|
reports.viewer | Read templates, reports, runs, artifacts (subject to property scope) | staff, manager, tenant.admin, tenant.owner |
reports.author | Create/update reports, save filters, queue ad-hoc runs | manager, tenant.admin, tenant.owner |
reports.scheduler | Create/update/delete schedules and subscriptions | manager (own property), tenant.admin, tenant.owner |
reports.template_publisher | Publish/archive template versions | tenant.admin, tenant.owner |
reports.regulatory_submitter | Trigger regulatory submissions, view receipts | tenant.admin, tenant.owner (jurisdiction-specific) |
reports.budget_admin | View AI cost projection per tenant | tenant.owner |
ABAC overlays:
- Property scope. Every report carries an effective property scope from
defaultFilters.propertyIdand request-time filters. We intersect that withjwt.propertyAccess[]; if the intersection is empty we reject withMELMASTOON.REPORTING.PROPERTY_SCOPE_VIOLATION. - Regulatory templates. Only users with
reports.regulatory_submitterand a matchingtenantUserAttributes.regulatoryJurisdictions[]may publish or run regulatory submissions for a given jurisdiction. - Recipient ownership. Subscription mutations from desktop are limited to rows where
recipient_user_id == jwt.userId(see SYNC_CONTRACT §4).
Authorization decisions are evaluated on the application layer entry point and again at the repository layer (defense-in-depth). All denials emit a structured audit event reporting.access_denied.
3. Tenant isolation
Three layers, each independently sufficient (ADR-0002):
- JWT claim.
tenantIdis part of the verified JWT and is set on the request context asreq.ctx.tenantId. Use cases reject any input containing a differenttenantId. - Postgres RLS. Every table in §3 of DATA_MODEL carries a
<table>_tenant_isolationpolicy. The connection pool runsSET LOCAL app.tenant_id = $1at the start of every transaction. - Object-storage prefix. Every GCS object key starts with
tnt_…/. The CHECK constraintobject_path LIKE tenant_id || '/%'makes a wrong-prefix write impossible at the DB layer. The signed-URL issuer also re-validates tenant prefix before signing.
The cross-tenant integration test (test/integration/tenant-isolation.spec.ts) must be present in CI and must fail-closed: with two tenants and a JWT for tenant A, every read endpoint must return zero rows from tenant B and every write must be rejected.
4. Data classification & encryption
| Field | Classification | Treatment |
|---|---|---|
| Template definitions, layouts, filters | Internal | TLS-only |
report_runs.resolvedFilters | Internal | TLS-only; structurally constrained to dimensional values |
report_subscriptions.recipient_email_enc | PII | Field-level AES-256-GCM with tenant-scoped DEK (key in Cloud KMS, ring melmastoon-reporting, key subscription-pii); DEK fetched per-tenant and cached for 5 min |
report_subscriptions.recipient_email_hash | Pseudonymized | HMAC-SHA-256 with tenant salt; allows lookup by hash without revealing email |
report_subscriptions.recipient_credentials_ref | Secret pointer | Real secret stored in Secret Manager; only the resource path is in DB |
| GCS artifacts | Tenant data | At-rest encryption with CMEK key projects/<p>/locations/<r>/keyRings/melmastoon-reporting/cryptoKeys/artifacts; transit TLS 1.2+ |
| Regulatory artifacts | Tenant data + legal hold | CMEK + bucket-level object lock (10y default retention) |
| Logs | Internal | PII redaction filter strips email/phone patterns; structured logger forbids raw subject envelopes |
KMS key access is granted to the reporting-service GSA only via Workload Identity. CMEK rotation is automated annually; old key versions remain decrypt-only.
5. Secret handling
- Source of truth: Google Secret Manager.
- Bootstrap: at startup we resolve a fixed list of secret resource paths into in-memory references; we do not write secrets to disk.
- Rotation: consumers refresh leases every 5 min; rotation is hot (no restart needed).
- Forbidden: committing secret values, embedding them in env vars in CI logs, logging credential payloads.
- Regulatory adapter creds: stored as
projects/<p>/secrets/reporting-regadapter-<jurisdiction>with rotation owned by the responsible jurisdiction tech lead.
CI runs gitleaks against the service repo; actionlint ensures workflows do not echo secrets.
6. Audit trail
Every successful and denied write emits an audit event to audit-service (07 §9). Events captured:
| Verb | Subject |
|---|---|
template.published | tpv_… |
template.archived | tpv_… |
report.created/updated/archived | rep_… |
run.requested/completed/failed/cancelled | run_… |
schedule.created/updated/disabled | sch_… |
subscription.created/cancelled/paused | sub_… |
regulatory.submission_started/succeeded/failed/manually_resolved | reg_… |
access.denied | resource id + permission + reason |
Audit events include actor, causedBy.correlationId, tenantId, ip, userAgent, aiProvenance?. The audit trail is appended to the daily Merkle anchor batch.
7. AI safety
- All AI calls flow through
ai-orchestrator-serviceper AI_INTEGRATION §1. - Prompts never include guest PII; only aggregated facts are sent.
- Outputs that influence delivered artifacts carry an "AI-generated, review before sharing" badge.
- Tenant admin can disable any reporting capability; the off-switch is enforced server-side.
- Drafted reports require human confirmation before being scheduled.
8. Threat model (focused)
| Threat | Mitigation |
|---|---|
| Cross-tenant read via crafted filter | RLS + JWT claim re-validation + property-scope check |
| Signed URL replay after revocation | URLs are short-lived (≤ 15 min); per-artifact cache TTL; revoke list re-checked on issue |
| Regulatory adapter credential leak | Stored in Secret Manager only; never logged; integration tests redact |
| OOM render of huge dataset | Streaming + row caps + per-template rowCap; renderer aborts on exceed and emits report.failed with MELMASTOON.REPORTING.RESULT_TOO_LARGE |
| Template injection (HTML/JS) into PDF | Renderer disables JavaScript; sanitizes layout strings; CSP default-src 'none' for embedded assets |
| Pub/Sub push spoofing | OIDC verification; audience pinned; replay protected by inbox dedupe |
| Scheduler abuse (per-tenant scheduling collision) | Token-bucket on schedule fires per tenant; soft fail with MELMASTOON.REPORTING.SCHEDULE_RATE_LIMITED and back-off |
| Report data exfiltration via subscription to attacker email | Subscription create requires reports.scheduler; out-of-tenant email domains allow-listed by tenant admin |
| Object-lock circumvention | We do not expose any DELETE on regulatory bucket; service account lacks storage.objects.delete on it |
9. Compliance
- GDPR / DPDP / KSA PDPL: PII inventory tracked in
docs/07 §11; regulated retention enforced by bucket lifecycle + DB CHECKs. - Data residency: Cloud SQL and GCS buckets are regional; deployment topology fans out per residency. Cross-region replication is forbidden for
regulatory_10y_objectlockartifacts. - Right to erasure: receives
melmastoon.tenant.deleted.v1; runspurge_tenant(tenantId)which: deletes operational rows, anonymizes regulatory rows (keeps anonymous aggregates needed for legal compliance), and removes prefetch references.
Cross-references: DATA_MODEL §3 RLS, API_CONTRACTS §0, docs/07.