CodamAIDocs
Topicdone

Modules register roles and attributes

How CDMS reports its roles and attributes to CIAS: embedded as a bean, standalone through /cias/fetch with its own role, and what happens on a rejection.

Variants
embeddedstandalonerejected

What this is about

CIAS invents no roles. Every module states for itself which roles it knows and which values it needs about a person. That list is called the declaration, and CIAS turns it into client roles in Keycloak, entries in the user profile and entries in its own catalog.

This page follows the path across both modules: where the list is created in CDMS, how it reaches CIAS, and what happens when CIAS does not accept it.

What CIAS does with an accepted declaration is under Modules register their roles and Reconciliation with Keycloak. This page is about the stretch before that.

Where the list in CDMS comes from

A CDMS project does not write its declaration by hand. The generator builds it from the models at build time, as the class RoleRegistryService.

From the model to the declaration
  1. 1
    Developer→Hub
    models customer and puts role names on the operations, for example crm-read
  2. 2
    Build
    collects every role a model requires for create, read, update, delete, download, history or rollback
    Plus every role that sits on a field. A field role CIAS does not know would be a role that works at runtime and can be granted to nobody.
  3. 3
    Build
    collects every attribute an attribute filter restricts rows by
    A project without attribute filters declares no attribute at all. An invented entry would have CIAS write a field into the user profile that no filter ever reads.
  4. 4
    Generator
    writes a class with two methods out of it: roles() and attributes()
    Result: That class is exactly what CIAS reads — as a bean or as JSON. There is only the one.

What the generator decides while doing so:

ItemHow it is derived
role keyexactly the name written in the model
display groupthe part before the first dash, so crm for crm-read. Without a dash: no group
display namewhat the role is required for in this project, per model with its operations
attributesoptional and multivalued, without a default value

Why the attributes are optional: a required attribute needs a default value in Keycloak, otherwise CIAS rejects the whole declaration. The only default a generator could write here would be an empty one — and that would fail the request just the same, after it had been put into every account in the realm. Why they are multivalued: the runtime filter splits the value into several and searches with them; declared single-valued, Keycloak would return exactly the shape the filter is not written for.

The two ways

How the declaration reaches CIAS

When: CDMS and CIAS run in the same process.

  1. 1
    CIAS
    looks up the bean whose name the configuration states, for example roleRegistryService
  2. 2
    CIAS→CDMS
    calls roles() and attributes()
    A method call. No network, no token, no timeout.
  3. 3
    CIAS
    gets the two lists

Result: If the bean returns null instead of an empty list, or throws, the module counts as unreadable — never as a module that no longer has any roles.

When: CDMS is a service of its own.

sequenceDiagram
    participant S as CIAS service
    participant C as CDMS service
    S->>C: GET /cias/fetch (reader token)
    C->>C: Does the caller hold a reader role?
    alt yes
        C-->>S: 200 { roles: [...], attributes: [...] }
    else no
        C-->>S: 403
    end

Result: Only 200 is an answer. Everything else means unreadable, and every case gets its own reason in the report.

The choice is stated in the CIAS configuration, one line per module:

codamai:
  cias:
    authorization:
      declarations:
        reader-client:
          token-uri: https://iam.example.com/realms/codamai/protocol/openid-connect/token
          client-id: ${CIAS_DECLARATION_CLIENT_ID}
          client-secret: ${CIAS_DECLARATION_CLIENT_SECRET}
        modules:
          - name: cias
            client: cias-backend
            bean: ciasIdentityRegistry      # in the same process
          - name: cdms
            client: cdms-backend
            url: https://cdms.internal/cias/fetch   # a service of its own

Both forms occur side by side: CIAS is a module itself and always reads its own declaration as a bean.

  • name says who a role belongs to. It has to be unique and must not change between runs.
  • client belongs to the deployment, not to the module. One process is one client, so every module in one process shares it. The hub backend, for instance, carries cias and cdms on the same client — which is exactly why it needs the name beside it.
  • Exactly one of bean and url. Both or neither fails the start, see Cold start of an installation.

The reader role on both sides

The endpoint serves the application’s complete permission map: every role a model requires, and every value a filter reads. That is nothing that may stand open.

What GET /cias/fetch checks
  1. CDMS
    Is there a token?
    Did the filter chain fill a caller context?
    ↳ no refused — a request without a token holds no roles
  2. CDMS
    Is there a reader role?
    Does the caller hold one of the realm roles in codamai.cdms.cias.reader-roles?
    ↳ no 403, without revealing which role would have been enough
  3. The two lists as JSON

Two things about this are easy to miss:

  • What is checked are realm roles, not the application’s business roles. The caller is CIAS, and it reads the same endpoint on every module; a client role would have to be granted once per client.
  • codamai.cdms.cias.reader-roles has no default. An application that states nothing does not start. An explicitly empty value is legitimate and means nobody — the right setting for an installation whose roles are not reconciled by CIAS at all.

On the other side CIAS needs a token carrying one of those roles. CIAS gets it from the IAM (Identity and Access Management, here Keycloak) itself, with a client of its own (reader-client). The procedure is called client credentials: the service signs in at the token endpoint with its client ID and secret and receives a token for itself, with no person involved. CIAS keeps the token for three quarters of its lifetime and then fetches a new one.

  • If no client is configured, CIAS uses a fixed token from reader-token. It does not refresh.
  • A client with a part missing (the secret, say) stops the startup.
  • If no token can be obtained, the module counts as unreadable for this run.

This call is not on a request path — it runs at startup and on request — so it may take seconds.

When CIAS does not accept

CIAS reads every module first, then checks everything, and only then writes. A pass that wrote while reading would apply a contradictory configuration by halves.

What a rejection affects
FindingReachConsequence
the module does not answer, or does not answer with 200this modulenothing about its roles changes, not even a retirement
required attribute without a default value, or one attribute described twice differentlythis modulethe whole declaration drops out of the pass, its roles included
a role key on this client already belongs to another modulethis clientnothing is written on this client, other clients run normally
two modules declare the same key on one clientthis clientas above
two modules describe one attribute differently in the same passeverythingthe pass writes nothing, for no module

The middle case is the one you are most likely to meet when two modules work together:

Two modules describing an attribute identically are not a contradiction; both may declare it. And a rejected declaration never stops CIAS: CIAS is what everybody registers with, and one module’s fault must not block the way to fixing it.

What ends up in Keycloak

Request
GET /cias/fetch
Authorization: Bearer <token with the reader role>
Response
{
  "roles": [
    { "key": "crm-read",  "group": "crm", "name": "Customer: read" },
    { "key": "crm-write", "group": "crm", "name": "Customer: create, update, delete" }
  ],
  "attributes": [
    { "key": "regionen", "multivalued": true, "defaultValue": null,
      "required": false, "selfEditable": false, "binding": "USER" }
  ]
}

Every role entry becomes a client role on the module’s client, plus a catalog entry with an owner and tenant scope. Every attribute entry becomes a field in the user profile and a claim mapper, so the value arrives in the token.

What the module does not decide: the client, the scope and the delegation. Those are set by the installation — a module must not decide who hands out its roles.

Watch out

Next

Sources in the code and the knowledge base
  • CDMS/cdms-generator – RoleRegistryProcessor (roles per model and field, group before the first dash, attributes from the attribute filters, optional and multivalued)
  • CDMS/cdms-authorization – CiasApi (GET /cias/fetch), CiasReaderRoles (codamai.cdms.cias.reader-roles, no default), Response
  • CIAS/cias-authorization – LocalModuleDeclarationAdapter, RemoteModuleDeclarationAdapter (only 200 counts), ModuleDeclarationPort, ModuleDeclarationSources, DeclarationReaderCredentials, ClientCredentialsDeclarationReaderCredentials (token from the IAM, renewed after three quarters of its lifetime)
  • CIAS/cias-authorization – RoleReconciliationService (defect, attributeContradictions, attributeTakenFromAnotherModule, keyCollisions, write), ReconciliationReport.Status, CiasIdentityRegistry
  • CIAS/cias-spring-boot-starter – CiasDeclarationAutoConfiguration (checks at startup, declarationReaderCredentials: reader-client before reader-token)
  • commons – IdentityRegistryInterface, models.Role, models.Attribute, models.AttributeBinding
  • CIAS/cias-runtime – application.yml (codamai.cias.authorization.declarations); hub-backend – application.yaml (two modules on one client)
Search