Skip to content

docs(adr0035): Add relation sync provider ADR - #1226

Merged
gtema merged 1 commit into
mainfrom
claude/vibrant-meitner-90ayvj
Sep 8, 2026
Merged

docs(adr0035): Add relation sync provider ADR#1226
gtema merged 1 commit into
mainfrom
claude/vibrant-meitner-90ayvj

Conversation

@gtema

@gtema gtema commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

ADR 0033 shipped the OpenFGA assignment driver with a documented hole:
nothing syncs Keystone group membership into the store, so a deployment
that switches to it issues tokens missing every group-derived role.
Operators querying OpenFGA from services other than Keystone need those
membership tuples too, and SCIM provisioning is where a tenant expects a
new team to become usable.

Propose an optional relation_sync provider with two independently
pluggable driver kinds, both on the existing BackendRegistration
machinery: dyn RelationSyncOutbox (ships sql) and
dyn RelationSyncTarget (ships openfga). With no [relation_sync]
section the provider is not built, no outbox row is written and no task
is spawned, so OpenFGA stays optional.

  • Membership mutations in identity-driver-sql move from a bare insert to
    a db.begin() transaction that also inserts outbox rows. The outbox is
    a new rust-only table; no python-owned table is altered, so a
    co-deployed python keystone is unaffected (the federated_identity_
    provider precedent).
  • An outbox row is a key, never a payload: the relay re-reads the
    authoritative membership and writes or deletes the tuple. That makes
    drains idempotent, order-insensitive and coalescing, which matters
    because Event carries no sequence and spawn-per-hook does not preserve
    ordering.
  • RelationSyncHook subscribes to the existing fire-and-forget dispatcher
    only to wake the relay early. It performs no I/O and no outbox write,
    so at-most-once delivery costs latency, never data.
  • A leader-gated reconciler diffs Keystone against each target per group
    and is the correctness mechanism, not a backstop: a co-deployed python
    keystone and LDAP-backed domains both write memberships that emit no
    event this process can observe.
  • Target credentials stay in server configuration, never the domain
    configuration API, for the reason ADR 0034 §6 gives; the membership
    relation is validated against role_to_relation so the relay cannot
    write relations that grant roles.
  • Metrics per ADR 0031, with relation_sync_lag_seconds called out as the
    security-relevant number since it bounds how long a revoked membership
    still authorizes in the target store.

Records the rejected alternatives: an inline write inside the membership
transaction, a hook that writes tuples directly, contextual tuples as a
substitute rather than a complement, splitting membership into its own
provider, and attributed membership edges.

Signed-off-by: Artem Goncharov artem.goncharov@gmail.com

ADR 0033 shipped the OpenFGA assignment driver with a documented hole:
nothing syncs Keystone group membership into the store, so a deployment
that switches to it issues tokens missing every group-derived role.
Operators querying OpenFGA from services other than Keystone need those
membership tuples too, and SCIM provisioning is where a tenant expects a
new team to become usable.

Propose an optional `relation_sync` provider with two independently
pluggable driver kinds, both on the existing BackendRegistration
machinery: `dyn RelationSyncOutbox` (ships `sql`) and
`dyn RelationSyncTarget` (ships `openfga`). With no [relation_sync]
section the provider is not built, no outbox row is written and no task
is spawned, so OpenFGA stays optional.

- Membership mutations in identity-driver-sql move from a bare insert to
  a db.begin() transaction that also inserts outbox rows. The outbox is
  a new rust-only table; no python-owned table is altered, so a
  co-deployed python keystone is unaffected (the federated_identity_
  provider precedent).
- An outbox row is a key, never a payload: the relay re-reads the
  authoritative membership and writes or deletes the tuple. That makes
  drains idempotent, order-insensitive and coalescing, which matters
  because Event carries no sequence and spawn-per-hook does not preserve
  ordering.
- RelationSyncHook subscribes to the existing fire-and-forget dispatcher
  only to wake the relay early. It performs no I/O and no outbox write,
  so at-most-once delivery costs latency, never data.
- A leader-gated reconciler diffs Keystone against each target per group
  and is the correctness mechanism, not a backstop: a co-deployed python
  keystone and LDAP-backed domains both write memberships that emit no
  event this process can observe.
- Target credentials stay in server configuration, never the domain
  configuration API, for the reason ADR 0034 §6 gives; the membership
  relation is validated against role_to_relation so the relay cannot
  write relations that grant roles.
- Metrics per ADR 0031, with relation_sync_lag_seconds called out as the
  security-relevant number since it bounds how long a revoked membership
  still authorizes in the target store.

Records the rejected alternatives: an inline write inside the membership
transaction, a hook that writes tuples directly, contextual tuples as a
substitute rather than a complement, splitting membership into its own
provider, and attributed membership edges.

Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
@gtema
gtema added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 0fd8e8e Sep 8, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant