Fix operational-party association bug in tenant provisioning

Table of Contents

This page is a capture in the discarded bucket of the product backlog.

Resolved (deep backlog refinement, 2026-07-11): Capture's own Update section documents root cause and fix; verified stamp_account_party() exists in projects/ores.iam/core/include/ores.iam.core/messaging/account_party_handler.hpp (lines 77, 192, 238), used in place of the generic stamp() in both save paths. filetags include :resolved:.

What

projects/ores.shell/src/app/commands/provision_commands.cpp's provision_commands::process_tenant, Phase 3 ("Associating '<user>' with the operational parties…", around line 397), is supposed to loop over every party with party_category = "Operational"= returned by refdata::messaging::get_parties_request and create one account_party association per party via iam.v1.account-parties.save. In a real test run (barclays_system_provision.ores, tenant barclays), log evidence from ores.iam.service.0.log around 2026-07-03 00:26:04 shows ~20 identical account_party_mapper "Mapping domain entity" trace lines, all with the exact same party_id — the tenant's auto-created System Party's UUID (4996676e-a823-4a30-a17a-08e828a769d2, party_category = "System", which the if (party.party_category ! "Operational") continue;= filter should have skipped) — never any of the ~81 distinct Operational parties that actually exist in that tenant (BARCLAYS PLC plus ~80 GLEIF-imported subsidiaries).

Net effect: after running the recipe, tenant_admin ends up associated only with the System Party, never with any real operational party. Since login auto-selects when an account has exactly one associated party (projects/ores.iam/core/include/ores.iam.core/messaging/auth_handler.hpp, the account_parties.size() = 1= branch), every subsequent login lands on System Party context regardless of which tenant/party the user thinks they're using — this presents as a login bug but is actually this association step silently doing the wrong thing.

Update — root cause found and fixed. The bug was not in provision_commands.cpp's loop at all (that code was already correct: distinct party.id per iteration). It was in the shared ores::service::messaging::stamp() helper (projects/ores.service/include/ores.service/messaging/handler_helpers.hpp): it unconditionally overwrites any domain object's party_id field from the caller's own JWT context, which is correct for entities where party_id means "which party owns this row" (a security boundary) but wrong for account_party, where party_id means "which party is being targeted by this association" — a client-supplied value. Because account_party happens to have a field literally named party_id, the generic reflection-based stamp() matched on name alone and clobbered every association with the caller's own current party (the System Party) before it reached the database — account_party_handler.hpp's save() called stamp(ap, ctx) on each row.

Fixed by adding a narrow stamp_account_party() in projects/ores.iam/core/include/ores.iam.core/messaging/account_party_handler.hpp that stamps tenant_id=/=modified_by=/=performed_by=/=change_reason_code but leaves account_id=/=party_id exactly as supplied by the client, used in place of the generic stamp() in both the synchronous and workflow-step save() paths. provision_commands.cpp's Phase 3 loop was not touched — it was already correct and should now work as intended.

Why

Discovered while implementing Synthetic data librarian support: FX foundation — a party-scoped synthetic FX config was correctly created in the database, but appeared invisible in the Qt client because the logged-in account was never actually linked to the party being tested, due to this bug.

References

  • projects/ores.service/include/ores.service/messaging/handler_helpers.hpp, stamp() — the generic helper whose party_id overwrite caused this.
  • projects/ores.iam/core/include/ores.iam.core/messaging/account_party_handler.hpp, stamp_account_party() — the fix.
  • projects/ores.iam/core/include/ores.iam.core/messaging/auth_handler.hpp, the account_parties.size() = 1= auto-select branch — explains why the symptom looked like a login bug.
  • doc/recipes/shell/provisioning/barclays_system_provision.org — the recipe that reproduces this; should be re-tested against a fresh DB once this fix is verified end to end.

See also

Emacs 29.3 (Org mode 9.6.15)