CodamAIDocs
Topicdone

Reconciliation with Keycloak

At startup and at the push of a button, CIAS brings roles, profile attributes and claim mappers in Keycloak up to date. Why Keycloak first and then the catalog, and why nothing is ever deleted but retired instead.

Variants
at startupon requestnew rolewithdrawn role → retiredrequired attribute without default → rejected

What this is about

Modules register their roles and attributes, see Modules register their roles. For this to have any effect, someone has to bring Keycloak and the catalog up to date. The reconciliation does that. It writes three things:

  • the user profile in Keycloak: which attributes an account can have
  • the client roles and the claim mappers per module. A claim mapper makes sure that an attribute arrives in the client’s token
  • the catalog in CIAS

When it runs

OccasionHowWho
at startupafter startup, when codamai.cias.authorization.startup.enabled=truethe application itself
on requestPOST /cias/admin/roles/reconcileonly a platform administrator, otherwise 403

There is no timer, on purpose. In Keycloak, the user profile is one document that is replaced completely on every write. Two runs at the same time would overwrite each other. So only one run runs at a time within a process: whoever presses the button while a run is going gets back a report “already running”, and nothing happens twice.

At startup, before the reconciliation, CIAS also creates the realm roles that the installation names in its configuration (startup.realm-roles, for example user). These never come from a declaration.

Three phases: read, reject, write

One run
  1. 1
    CIAS
    read: query every configured declaration; a module that cannot be reached is noted and otherwise left alone
  2. 2
    CIAS
    reject: faulty declarations drop out, attribute conflicts stop the whole run, key collisions stop their client
  3. 3
    CIAS→Keycloak
    write 1: the user profile, once for all modules
  4. 4
    CIAS→Keycloak
    write 2: per module, create missing client roles, then the claim mappers
  5. 5
    CIAS
    write 3: per module, the catalog, in its own transaction
    Result: One report per module: what was created, changed, retired, reinstated

First read everything, then check everything, then write. A run that already wrote while reading would apply a conflicting configuration halfway, and afterwards nobody would know which half. The rejections in detail are described in When CIAS does not accept a declaration.

Keycloak first, then the catalog

Keycloak and CIAS have no shared transaction. If something fails between the two, one half is left alone. The order decides which one:

What is left when the run fails in the middle

When: The role is created in Keycloak, the catalog is not written anymore.

A role without holders in Keycloak changes nothing for anyone. The next run adds it to the catalog.

Result: harmless

When: The role is in the catalog but missing in Keycloak.

An administrator grants it, CIAS stores the grant, and the token still does not carry the role.

Result: a permission that CIAS offers and cannot deliver

If creating in Keycloak fails for a module, the report shows it as UNPROVISIONED, and its catalog stays unchanged, including renames and retirements. The next run catches up on everything at once, because every step can be repeated.

What happens to a module’s roles

Before and after, per role

When: The module registers order-export, the catalog does not know it.

Created in Keycloak, entered in the catalog: owner is the module, scope TENANT, delegation from the installation's configuration.

Result: in the report under defined

When: The module registers order-edit with a new display name.

Display name and display group are taken over. Description and delegation stay as an administrator set them. The module says nothing about those.

Result: in the report under updated

When: The module no longer registers order-archive.

The role is retired, with date and time. It stays in Keycloak, and all grants stay. It can no longer be granted again.

Result: in the report under deprecated

When: The module registers a retired role again.

The retirement is lifted. The grants were never gone, so nothing has to be given back.

Result: in the report under reinstated

When: The module does not answer.

Nothing changes, not even a retirement. A role is never retired just because its service is restarting.

Result: UNREADABLE

Why retire instead of delete: if CIAS deleted a withdrawn role, every person who has it would lose their permission, just because a module cleaned up its list. And nobody could check later that the role ever existed.

Only what belongs to this module is retired. If several modules share a client, the silence of one never retires the roles of the other. Roles without an owner, for example the installation’s realm roles, are never retired by a reconciliation.

Attributes

The same principle applies to attributes, only stricter:

  • An attribute that no module registers anymore stays in the user profile. Deleting it would destroy the value in every account. A role is a permission, an attribute is content.
  • CIAS rejects a required attribute without a default value, together with the whole declaration of this module. Otherwise, accounts that existed before would have no value. The profile would then claim that such accounts cannot exist, and after that Keycloak would even refuse changes to these accounts, including suspending them.

More in Registering attributes.

The report

Request
POST /cias/admin/roles/reconcile
Authorization: Bearer <token of a platform administrator>
Response
{
  "applied": true,
  "modules": [
    { "module": "cias", "client": "cias-backend", "status": "RECONCILED",
      "provisioned": [], "defined": [], "updated": [],
      "deprecated": [], "reinstated": [], … },
    { "module": "cdms", "client": "cdms-backend", "status": "RECONCILED",
      "provisioned": ["cdms-backend/order-export"],
      "defined": ["cdms-backend/order-export"],
      "deprecated": ["cdms-backend/order-archive"], … }
  ],
  "attributes": [],
  "refusals": []
}
StatusMeaning
RECONCILEDread, created in Keycloak, catalog written
UNPROVISIONEDcreating in Keycloak failed, catalog unchanged
UNREADABLEmodule could not be queried, nothing changed
REJECTEDdeclaration not acceptable, none of it applied
REFUSEDkey collision on the client, nothing written on this client
SKIPPEDthe whole run wrote nothing, the reason is in refusals

The report always shows roles as client/key, because the key alone no longer says which role is meant.

Pitfalls

Next

Sources in the code and the knowledge base
  • CIAS/cias-authorization – RoleReconciliationService (execute: read, reject, write; writeProfile, provision, deliver, write; ReentrantLock), ReconciliationReport (applied, modules, attributes, refusals, Status)
  • CIAS/cias-authorization – RoleStartupPass (the installation's realm roles, then reconciliation; never throws), CiasAuthorizationConfiguration (codamai.cias.authorization.startup.enabled, startup.realm-roles)
  • CIAS/cias-authorization – RoleAdminController (POST /cias/admin/roles/reconcile), Role (deprecate, reinstate)
  • CIAS/cias-iam-api – ClientRoleManagementPort.defineRole, UserProfileManagementPort, ClaimMappingPort
  • CIAS/cias-authorization/docs/adr – ADR-023 (sections 6 and 7), ADR-026, ADR-028, ADR-040
Search