Strict Service Table Isolation
Mandatory role drops + zero cross-component DB access
Table of Contents
- Context
- Goal and Invariant
- Phase 0 — Mandatory Role Drops
- Phase 1 — SECURITY DEFINER Validators + Grant Cleanup
- Phase 2 — IAM Cross-Component Reads
- Phase 3 — Trading Refdata Trigger Validation
- Phase 3a — Trading reads FSM transitions via NATS
- Phase 4 — Lock the Invariant
- Deferred Items
- Bootstrap Concerns
- Sequencing and Effort
- Open Questions
- File Pointers
Context
Two findings in Bootstrap and Architecture Violations motivate this plan:
recreate_database.shdoes not drop cluster-level roles, so historical group memberships silently mask missing per-service GRANTs. The system is therefore not reproducible across machines.- Several service users hold cross-component table grants. The Service Account RBAC Design (2026-03-28-service-rbac-design.org) stipulated that cross-component access should be the exception.
This plan establishes a strict architectural invariant — a domain service user holds DB privileges on its own component's tables only — and lays out an incremental migration to reach it.
The heavier architectural work (IAM party cache, workflow/ORE NATS write APIs) is tracked separately in cross-service-write-decoupling.org.
Goal and Invariant
For every domain service S with DB user ores_<env>_<S>_service:
ShasSELECT/INSERT/UPDATE/DELETEonly on tables matchingores_<S>_%in thepublicschema.ShasUSAGE/SELECTon sequences only for its own tables.Shas no grants on any table prefixed with another component.Shas no membership of the broadrw_roleorowner_role.- Cross-component validation (tenant existence, change-reason validity,
refdata entity existence) is enforced by
SECURITY DEFINERfunctions owned by the defining service — noSELECTgrant on cross-component tables is needed by the calling service user.
Key clarifications
- FK constraints do not require
SELECTon the referenced table at runtime. TheREFERENCESprivilege is needed only to create a FK (done by the DDL user); at runtime the check is system-internal. - Trigger-called validator functions that SELECT from another service's
tables must be
SECURITY DEFINERwithSET search_path = public, pg_temp. New validator functions must follow the same rule.
Non-goals
- Splitting
publicinto per-component PostgreSQL schemas. - Removing C++ type-sharing between components (only DB access paths must move).
- Touching unit tests that use the DDL user directly.
Phase 0 — Mandatory Role Drops
Status: COMPLETE (commit 6eb3930e4)
recreate_database.sh now drops all environment roles before recreating
them, ensuring reproducible cluster state regardless of history.
Phase 1 — SECURITY DEFINER Validators + Grant Cleanup
Status: COMPLETE (commit f94fda946)
ores_iam_validate_tenant_fn and ores_dq_validate_change_reason_fn
converted to SECURITY DEFINER. 24 cross-component SELECT grants removed.
Two confirmed-incorrect grants (compute→refdata_parties,
reporting→scheduler prefix) also removed.
Remaining cross-component grants after Phase 1:
- IAM: SELECT
ores_variability_system_settings_tbl(Phase 2.1) - IAM: DML
ores_refdata_parties(Phase 2.2 + deferred plan) - Trading: SELECT
ores_refdata_*prefix (Phase 3) - Workflow: DML
ores_iam_*andores_refdata_parties(deferred plan) - ORE: DML
ores_workflow_*(deferred plan)
Phase 2 — IAM Cross-Component Reads
Effort: S–M. Risk: Low–Medium.
2.1 — IAM reads variability settings via SECURITY DEFINER function
Status: COMPLETE
IAM reads ores_variability_system_settings_tbl at runtime for bootstrap
mode, JWT token settings, and signup policy. These are generic runtime
configuration values owned by variability; IAM should not hold a DB grant
to read them directly.
Fix: ores_variability_get_system_settings_fn (SECURITY DEFINER) wraps the
SELECT; system_settings_service::refresh() calls read_for_tenant when a
tenant_id is set, bypassing the need for a direct SELECT grant.
Steps:
- Variability exposes a new NATS request/reply subject
variability.v1.system-settings.readthat returns the requested keys. Variability also publishes tovariability.v1.system-settings.changedwhen any value is written (LISTEN/NOTIFY already triggers this). - In
bootstrap_mode_service.cppand the relevant handlers, replace the directsystem_settings_servicerepository call with a NATS request at startup. Cache the result in-process and refresh on change notification. - Remove
grant select on ores_variability_system_settings_tbl to :iam_service_userfrom the service registry. - Remove the CMake link from
ores.iam.core/src/CMakeLists.txttoores.variability.coreonce no IAM source files reference it.
2.2 — Provisioning returns the system_party_id
bootstrap_handler.hpp:203 calls
party_repository.read_system_party(...) immediately after
ores_iam_provision_tenant_fn, solely to retrieve the party id that the
provisioning function just created.
Fix: return it from the function.
Steps:
- Modify
ores_iam_provision_tenant_fntoRETURNS RECORDwith(tenant_id uuid, system_party_id uuid). - Update
bootstrap_handler.hppto consume both fields and remove theparty_repository.read_system_partycall. - Verify
save_account_partystill works — it inserts intoores_iam_account_parties_tbl(IAM-owned) and uses the party id now returned from provisioning, so no cross-service read is needed. - After this step the only remaining IAM DML grant on
ores_refdata_partiesis for the hot-path party reads inauth_handler.hpp, which are tracked in the deferred plan.
Phase 3 — Trading Refdata Trigger Validation
Effort: S. Risk: Low.
Trading table triggers contain inline SELECT statements against refdata
tables (books, portfolios, counterparties, parties, business_units)
for soft-FK validation. Because these run in a SECURITY INVOKER context,
the trading service user holds a broad SELECT ON ores_refdata_* prefix grant.
Fix: wrap each inline SELECT in a named SECURITY DEFINER function.
Steps:
In
projects/ores.sql/create/refdata/create a new filerefdata_trading_validation_fns_create.sqldefining:ores_refdata_validate_book_fn(tenant_id uuid, book_id uuid)ores_refdata_validate_portfolio_fn(tenant_id uuid, portfolio_id uuid)ores_refdata_validate_counterparty_fn(tenant_id uuid, counterparty_id uuid)ores_refdata_validate_party_fn(tenant_id uuid, party_id uuid)ores_refdata_validate_business_unit_fn(tenant_id uuid, bu_id uuid)
Each function is
SECURITY DEFINERwithSET search_path = public, pg_temp.Replace the inline
SELECTblocks in:trading_trades_create.sqltrading_trades_functions_create.sqltrading_trades_bu_functions_create.sqltrading_identifiers_create.sqltrading_party_roles_create.sql
with calls to the corresponding
SECURITY DEFINERfunctions.- Remove
select _ores_grant_select_fn('ores_refdata_', :'trading_service_user')from the service registry and regenerate the grants file.
Phase 3a — Trading reads FSM transitions via NATS
Status: COMPLETE
trade_status_service.cpp previously called
dq::repository::fsm_transition_repository::find_by_id() directly, violating
service isolation.
Fix applied:
trade_handler::save()callsdq.v1.fsm-transitions.listonce per request via the privatefetch_fsm_transitions()helper, returning aservice::fsm_transition_mapkeyed by UUID.- The map is passed into
trade_service::save_trades()→trade_status_service::resolve_status(), which looks up the transition by UUID without any DB access toores_dq_fsm_transitions_tbl. #include "ores.dq.core/repository/fsm_transition_repository.hpp"removed fromtrade_status_service.cpp.
Phase 4 — Lock the Invariant
Status: COMPLETE
Every domain service user now holds DML only on its own ores_<S>_* prefix,
with no cross-component SELECT grants. The three remaining cross-component DML
grants (IAM→refdata parties, workflow→IAM/refdata, ORE→workflow) require NATS
write APIs and are deferred to
cross-service-write-decoupling.org.
CLAUDE.md and system_model.org reflect current state.
Deferred Items
The following items require significant NATS API work and multi-service refactoring. They are tracked in a separate plan: cross-service-write-decoupling.org
- IAM hot-path party reads (
auth_handler,account_handler) replace directores_refdata_parties_tblaccess with a NATS-backed cache. - Workflow writes to IAM and refdata tables during onboarding → replace with IAM and refdata NATS write APIs.
- ORE import writes to workflow tables → replace with workflow NATS write APIs.
Bootstrap Concerns
Bootstrap remains the one place where IAM legitimately crosses domain boundaries. After this plan:
SECURITY DEFINERprovisioning function continues to handle tenant + system-party creation in a single transaction.- After Phase 2.2, the function returns
system_party_id, so IAM no longer readsores_refdata_parties_tblduring provisioning. - Bootstrap-mode gating is read via NATS from variability (Phase 2.1), so no DB grant is needed.
Sequencing and Effort
| Phase | Title | Effort | Risk | Status |
|---|---|---|---|---|
| 0 | Mandatory role drops | S | Low | COMPLETE |
| 1 | SECURITY DEFINER validators + cleanup | S | Low | COMPLETE |
| 2.1 | IAM settings via SECURITY DEFINER fn | S | Low | COMPLETE |
| 2.2 | Provisioning returns system_party_id | S | Low | COMPLETE |
| 3 | SECURITY DEFINER for trading trigger fns | S | Low | COMPLETE |
| 4 | Lock invariant | S | Low | COMPLETE |
| — | IAM party cache (deferred) | L | Medium | deferred |
| — | Workflow/ORE NATS write APIs (deferred) | L | High | deferred |
Open Questions
- Schema-per-component. Moving to per-component Postgres schemas would make GRANTs trivial. Out of scope here; natural next step once the invariant holds.
- Trading refdata coupling. The SECURITY DEFINER function approach keeps DB-level referential integrity for trading entities. Consider whether this is the right long-term model or whether trading should hold a local refdata cache instead.
- Synthetic service. Currently classified
role: "tooling"with broad SELECT on all domains. The cleaner option is to run it as the DDL user.
File Pointers
| Concern | File |
|---|---|
| Mandatory drops | projects/ores.sql/recreate_database.sh, drop_roles.sql |
| Service registry | projects/ores.codegen/models/services/ores_services_service_registry.json |
| GRANT matrix | projects/ores.sql/create/iam/iam_service_db_grants_create.sql |
| Tenant validator fn | projects/ores.sql/create/iam/iam_tenant_functions_create.sql |
| Change reason validator fn | projects/ores.sql/create/dq/dq_change_reason_functions_create.sql |
| Trading trigger refdata SELECTs | projects/ores.sql/create/trading/trading_trades_create.sql et al. |
| IAM handlers (party + settings) | projects/ores.iam.core/include/ores.iam.core/messaging/ |
| Bootstrap handler | projects/ores.iam.core/include/ores.iam.core/messaging/bootstrap_handler.hpp |
| Predecessor analysis | doc/analysis/bootstrap-and-architecture-violations.org |
| Deferred write-decoupling plan | doc/plans/2026-05-13-cross-service-write-decoupling.org |