CodamAIDocs
Topicdone

Ports and adapters

The business logic talks to interfaces (“ports”), an adapter translates for Keycloak. Which ports exist and why a second provider is a second artifact.

Variants
Keycloak adapter selectedin-memory adapter selectedno provider selectedprovider unreachableprovider reports a conflictprovider does not know the objectprovider refusesa second provider

What this is about

CIAS writes a lot into the identity provider (IdP for short), that is, into Keycloak: accounts, organizations, roles, groups, attributes. Still, none of the business modules knows Keycloak. Registration does not know Keycloak’s URL, and role granting does not know how Keycloak stores a role.

That comes from a split into two parts:

  • A port is a Java interface. It only says what is needed: “create an account”, “grant this role in this tenant”.
  • An adapter is a class that fulfils this interface for one specific system. It knows how to do it in Keycloak: which HTTP call, which fields, which answer.

The picture

flowchart LR
    subgraph BM["Business modules"]
      R["cias-registration"]
      U["cias-user"]
      A["cias-authorization"]
    end
    subgraph P["cias-iam-api: ports"]
      P1["Accounts"]
      P2["Organizations"]
      P3["Roles"]
      P4["Groups"]
      P5["Attributes"]
    end
    R --> P
    U --> P
    A --> P
    P --> KA["cias-iam-keycloak<br/>Keycloak adapter"]
    P --> MA["cias-iam-memory<br/>in-memory adapter"]
    KA -- "Admin REST API" --> K["Keycloak"]
    MA --> M[("Memory<br/>in the process")]

Read it from left to right: the business modules only know the middle. On the right there is exactly one adapter, depending on the setting. The arrows only point in one direction. No business module goes past the middle to Keycloak. An architecture test in every business module checks this: a test that fails the build as soon as a class imports a Keycloak package.

The ports

There are eleven ports. Each one covers one capability, not a whole system. A module that only looks up addresses does not depend on role management.

PortWhat it can doWho uses it
IdentityProvisioningPortcreate an account disabled, mark the address as verified, enable, disable, require “set password”, get a password link, delete during cleanupRegistration, Users
IdentityLookupPortfind an account by email address or by IDRegistration, Users
IdentityDirectoryPortread all accounts page by pageUsers (import)
IdentityAttributePortset or remove one attribute on one accountRegistration, Users
OrganizationManagementPortcreate an organization, find it by its alias, delete it during cleanupRegistration
MembershipManagementPortgrant, revoke and read membership in an organizationRegistration, Users
RoleManagementPortrealm roles: check, create, grant globally or in an organization, revoke, readRoles, Registration
ClientRoleManagementPortthe same for the roles of a client, that is, module rolesRoles, Registration
GroupManagementPortmaintain the copy of a CIAS group in KeycloakGroups
UserProfileManagementPortwhich attributes an account may carry at allRoles and attributes (reconciliation)
ClaimMappingPortget an attribute into the tokens of a clientRoles and attributes (reconciliation)

Three terms from the table:

  • A realm role applies in the whole realm and therefore in all modules, for example platform-admin. A client role belongs to one client and only means something there, for example a role of CDMS. See Realm role, client role, organization role.
  • An organization is how Keycloak represents a dynamic tenant. See Static and dynamic tenants.
  • The user profile is a document per realm in Keycloak that defines which attributes an account may have.

The rules behind the ports

The ports are cut this way on purpose. Each rule prevents a specific mistake.

  • No Keycloak type in a port. No signature and no exception class names Keycloak. The port only knows its own small records such as NewIdentity or IamIdentity.
  • No password. There is “require set password”, but no “set password”. CIAS never accepts a password.
  • No user name. The email address is the login. One account per address, any number of tenants.
  • Global and “in one organization” are separate methods. assign grants platform-wide, assignInOrganization only in one tenant. If both were one method with an optional tenant, a tenant administrator could give themselves platform-wide rights by accident.
  • Realm roles and client roles are separate ports. For the same reason: through the port for client roles you cannot reach a realm role.
  • Every call may come twice. Granting a role that is already there changes nothing. Enabling an account that is already enabled does not either. There is no shared transaction between CIAS and Keycloak. If an answer gets lost, repeating is the only way, and repeating must not break anything.
  • A new capability is a new port. No catch-all interface with thirty methods.
  • An attribute needs three steps, each in its own port. The profile allows it (UserProfileManagementPort), an account gets a value (IdentityAttributePort or at creation), and the value gets into the token (ClaimMappingPort). See The path into the token.

References instead of keys

Keycloak hands out its own IDs. CIAS keeps them in small wrappers: ExternalUserId, ExternalOrganizationId, ExternalGroupId, ExternalRoleId. These are only references: “this thing lives over there in the IdP”.

The business identity stays in CIAS:

Objectidentified in business terms byfound again in Keycloak via
Persontheir CIAS IDExternalUserId
Tenantthe tenant keyalias of the organization = tenant key
Groupthe group keyname of the group = group key
Roleits namename of the role

So whoever replaces the IdP changes references, not data.

When Keycloak does not do something

Every port reports problems with one of four failure kinds. The business modules react to the kind, not to an HTTP status.

The four failure kinds

When: Keycloak does not answer, answers with a server error, or CIAS does not get a login token for itself.

IamUnavailableException. The only kind that may be retried. Nobody knows whether the call already took effect, and that is why every call is built so that it may come twice.

Result: caller waits or tries again later

When: There is already an account with this address, an organization with this alias, or a group with this name.

IamConflictException. Keycloak enforces uniqueness. This message must never reach a person who is not logged in unfiltered, or a public endpoint would reveal which addresses exist.

Result: caller decides, often: carry on as if the lookup had found the object

When: CIAS refers to an account, a role, a client or a group that does not exist (anymore) in Keycloak.

IamNotFoundException. Usually a sign that CIAS and Keycloak have drifted apart, for example because someone deleted something in the Keycloak console.

Result: error, often a case for a reconciliation

When: Keycloak refuses for another reason, or CIAS is asked to change something that does not belong to CIAS.

IdentityProvisioningException. The honest catch-all: do not retry, record it and stop.

Result: error, the operation stays in a recognizable state

Lookups do not report “not found” as an error but as an empty result. “There is no account for this address” is the normal answer on a public endpoint.

Which adapter runs

Two things have to come together: the adapter’s jar is on the classpath, and the setting names it.

codamai.cias.iam-provider

When: codamai.cias.iam-provider: keycloak, and cias-iam-keycloak is on the classpath.

CIAS talks to Keycloak through the Admin REST API. Where Keycloak is and which service account CIAS logs in with is set under codamai.cias.keycloak.*. See The Keycloak adapter.

Result: the normal case of every installation

When: codamai.cias.iam-provider: memory, and cias-iam-memory is on the classpath.

All ports work on one store in the process's memory. After a restart everything is gone. See The in-memory adapter for tests and development.

Result: tests and local development

When: The setting is missing, or it names an adapter whose jar is missing.

There are no ports. The application does not start and names the missing port. That is intended: a CIAS without an IdP would accept registrations it can never carry out.

Result: no start

Which adapter is active?
Value of iam-providerJar on the classpath?Result
keycloakyesKeycloak adapter
memoryyesin-memory adapter
keycloak or memorynono ports, no start
empty–no ports, no start

If both jars are on the classpath, still only the one the setting names runs. The standalone CIAS ships both: the local profile sets memory, everything else keycloak.

A second provider is a second artifact

If CIAS is ever to work with a different IdP, nobody changes the business modules. Instead, a new module is created, for example cias-iam-<name>:

What a new adapter needs
  1. 1
    Developer
    creates a module of its own that only knows cias-iam-api and the library of the new IdP
  2. 2
    Developer
    implements the ports the new IdP supports and translates its errors into the four failure kinds
  3. 3
    Build
    runs the same contract tests as for Keycloak and the in-memory store
  4. 4
    Developer
    gives the adapter a name for codamai.cias.iam-provider and wires it
    Result: business modules unchanged, only the setting changes

Why a separate artifact and not a switch inside the Keycloak adapter? Every adapter brings the libraries of its IdP. If two IdPs lived in one module, every installation would depend on both. And the contract tests check exactly one adapter at a time.

Not every IdP can do everything. Organizations, roles in organizations or roles on groups are capabilities of Keycloak. An IdP without organizations cannot carry dynamic tenants.

Pitfalls

Next

Sources in the code and the knowledge base
  • CIAS/cias-iam-api – ClaimMappingPort, ClientRoleManagementPort, GroupManagementPort, IdentityAttributePort, IdentityDirectoryPort, IdentityLookupPort, IdentityProvisioningPort, MembershipManagementPort, OrganizationManagementPort, RoleManagementPort, UserProfileManagementPort
  • CIAS/cias-iam-api – model (ExternalUserId, ExternalOrganizationId, ExternalGroupId, ExternalRoleId, NewIdentity, IamIdentity, NewOrganization, IamOrganization, IamGroup, ProfileAttribute, PasswordSetupLink), exception (IamException, IamUnavailableException, IamConflictException, IamNotFoundException, IdentityProvisioningException)
  • CIAS/cias-spring-boot-starter – CiasIamAutoConfiguration (iamPorts, KeycloakConfiguration, MemoryConfiguration), CiasProperties (iam-provider)
  • CIAS/cias-runtime – application.yml, application-local.yml (codamai.cias.iam-provider)
  • CIAS/*/src/test – ArchitectureTest per business module
  • CIAS/cias-iam-api/docs/adr – ADR-004, ADR-039
Search