Skip to main content

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

CallerMechanism
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/fireOIDC-signed token, audience pinned, iss = accounts.google.com, service-account allowlist enforced
Pub/Sub push subscriptionsOIDC token verified, aud = https://reporting.melmastoon.ghasi.io
GCS signed URLsService-issued, V4 signature, ≤ 15 min TTL
External regulatory adaptersOAuth2 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:

PermissionAllowsDefault roles
reports.viewerRead templates, reports, runs, artifacts (subject to property scope)staff, manager, tenant.admin, tenant.owner
reports.authorCreate/update reports, save filters, queue ad-hoc runsmanager, tenant.admin, tenant.owner
reports.schedulerCreate/update/delete schedules and subscriptionsmanager (own property), tenant.admin, tenant.owner
reports.template_publisherPublish/archive template versionstenant.admin, tenant.owner
reports.regulatory_submitterTrigger regulatory submissions, view receiptstenant.admin, tenant.owner (jurisdiction-specific)
reports.budget_adminView AI cost projection per tenanttenant.owner

ABAC overlays:

  • Property scope. Every report carries an effective property scope from defaultFilters.propertyId and request-time filters. We intersect that with jwt.propertyAccess[]; if the intersection is empty we reject with MELMASTOON.REPORTING.PROPERTY_SCOPE_VIOLATION.
  • Regulatory templates. Only users with reports.regulatory_submitter and a matching tenantUserAttributes.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):

  1. JWT claim. tenantId is part of the verified JWT and is set on the request context as req.ctx.tenantId. Use cases reject any input containing a different tenantId.
  2. Postgres RLS. Every table in §3 of DATA_MODEL carries a <table>_tenant_isolation policy. The connection pool runs SET LOCAL app.tenant_id = $1 at the start of every transaction.
  3. Object-storage prefix. Every GCS object key starts with tnt_…/. The CHECK constraint object_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

FieldClassificationTreatment
Template definitions, layouts, filtersInternalTLS-only
report_runs.resolvedFiltersInternalTLS-only; structurally constrained to dimensional values
report_subscriptions.recipient_email_encPIIField-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_hashPseudonymizedHMAC-SHA-256 with tenant salt; allows lookup by hash without revealing email
report_subscriptions.recipient_credentials_refSecret pointerReal secret stored in Secret Manager; only the resource path is in DB
GCS artifactsTenant dataAt-rest encryption with CMEK key projects/<p>/locations/<r>/keyRings/melmastoon-reporting/cryptoKeys/artifacts; transit TLS 1.2+
Regulatory artifactsTenant data + legal holdCMEK + bucket-level object lock (10y default retention)
LogsInternalPII 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:

VerbSubject
template.publishedtpv_…
template.archivedtpv_…
report.created/updated/archivedrep_…
run.requested/completed/failed/cancelledrun_…
schedule.created/updated/disabledsch_…
subscription.created/cancelled/pausedsub_…
regulatory.submission_started/succeeded/failed/manually_resolvedreg_…
access.deniedresource 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-service per 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)

ThreatMitigation
Cross-tenant read via crafted filterRLS + JWT claim re-validation + property-scope check
Signed URL replay after revocationURLs are short-lived (≤ 15 min); per-artifact cache TTL; revoke list re-checked on issue
Regulatory adapter credential leakStored in Secret Manager only; never logged; integration tests redact
OOM render of huge datasetStreaming + row caps + per-template rowCap; renderer aborts on exceed and emits report.failed with MELMASTOON.REPORTING.RESULT_TOO_LARGE
Template injection (HTML/JS) into PDFRenderer disables JavaScript; sanitizes layout strings; CSP default-src 'none' for embedded assets
Pub/Sub push spoofingOIDC 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 emailSubscription create requires reports.scheduler; out-of-tenant email domains allow-listed by tenant admin
Object-lock circumventionWe 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_objectlock artifacts.
  • Right to erasure: receives melmastoon.tenant.deleted.v1; runs purge_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.