Keycloak · Arazzo Workflow

Keycloak Offboard a User

Version 1.0.0

Disable a user, strip realm role mappings and group membership, and optionally delete the account.

1 workflow 1 source API 1 provider
View Spec View on GitHub AuthenticationAuthorizationIdentity ManagementOAuthOpenID ConnectSecuritySSOArazzoWorkflows

Provider

keycloak

Workflows

offboard-user
Disable a realm user, remove its roles and group membership, and optionally delete it.
Resolves the user by username, disables the account, strips realm-level role mappings and group membership, and deletes the account only when hardDelete is true.
8 steps inputs: hardDelete, realm, username outputs: accountEnabled, formerGroups, revokedRealmRoles, userId, username
1
resolveUser
getUsers
Resolve the account UUID from the username. Every subsequent admin call is keyed on the UUID, and a miss here stops the flow before anything is changed.
2
disableUser
updateUser
Disable the account first. This is the step that actually stops access, so it runs before the slower entitlement teardown rather than after it.
3
listRoleMappings
getUserRealmRoleMappings
Read the realm-level role mappings currently granted to the user. The removal endpoint needs these representations verbatim in its body, so they have to be captured before they can be revoked.
4
stripRoleMappings
deleteUserRealmRoleMappings
Revoke every realm-level role captured in the previous step by passing the role representations back to the delete-mappings endpoint.
5
listGroupMemberships
getUserGroups
Read the groups the user belongs to, since group membership carries roles of its own and survives a disabled account.
6
removeFromPrimaryGroup
removeUserFromGroup
Remove the user from the first group returned. Keycloak exposes group removal one group at a time, so a user in several groups needs this workflow re-run per group id.
7
evaluateDeletion
getUser
Re-read the account to confirm it is disabled, and stop here unless the caller explicitly asked for a hard delete.
8
deleteUserAccount
deleteUser
Permanently delete the stripped account. Only reached when hardDelete is true; this cannot be undone.

Source API Descriptions

Arazzo Workflow Specification

Raw ↑
arazzo: 1.0.1
info:
  title: Keycloak Offboard a User
  summary: Disable a user, strip realm role mappings and group membership, and optionally delete the account.
  description: >-
    The deprovisioning counterpart to onboarding, ordered the way a careful
    operator actually runs it: disable the account first so access stops
    immediately, then unwind the entitlements that outlive a disabled account,
    and only then decide whether to delete. Role mappings are read before they
    are removed, because the Keycloak delete-mappings endpoint takes the role
    representations to remove in its request body rather than a role name. The
    destructive final delete is gated behind an explicit hardDelete input so the
    default outcome is a disabled, stripped, but recoverable account. Every step
    spells out its request inline so the flow can be read and executed without
    opening the underlying OpenAPI description.
  version: 1.0.0
sourceDescriptions:
- name: keycloakAdminApi
  url: ../openapi/keycloak-admin-rest-api-openapi.yml
  type: openapi
workflows:
- workflowId: offboard-user
  summary: Disable a realm user, remove its roles and group membership, and optionally delete it.
  description: >-
    Resolves the user by username, disables the account, strips realm-level role
    mappings and group membership, and deletes the account only when hardDelete
    is true.
  inputs:
    type: object
    required:
    - realm
    - username
    properties:
      realm:
        type: string
        description: The name of the realm the user belongs to.
      username:
        type: string
        description: The username of the account to offboard.
      hardDelete:
        type: boolean
        description: >-
          When true the account is permanently deleted after being stripped. When
          false (the default posture) the account is left disabled and empty but
          recoverable.
  steps:
  - stepId: resolveUser
    description: >-
      Resolve the account UUID from the username. Every subsequent admin call is
      keyed on the UUID, and a miss here stops the flow before anything is
      changed.
    operationId: getUsers
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: username
      in: query
      value: $inputs.username
    - name: max
      in: query
      value: 1
    successCriteria:
    - condition: $statusCode == 200
    - context: $response.body
      condition: $.length > 0
      type: jsonpath
    outputs:
      userId: $response.body#/0/id
      resolvedUsername: $response.body#/0/username
      userEmail: $response.body#/0/email
  - stepId: disableUser
    description: >-
      Disable the account first. This is the step that actually stops access, so
      it runs before the slower entitlement teardown rather than after it.
    operationId: updateUser
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: userId
      in: path
      value: $steps.resolveUser.outputs.userId
    requestBody:
      contentType: application/json
      payload:
        username: $steps.resolveUser.outputs.resolvedUsername
        enabled: false
    successCriteria:
    - condition: $statusCode == 204
  - stepId: listRoleMappings
    description: >-
      Read the realm-level role mappings currently granted to the user. The
      removal endpoint needs these representations verbatim in its body, so they
      have to be captured before they can be revoked.
    operationId: getUserRealmRoleMappings
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: userId
      in: path
      value: $steps.resolveUser.outputs.userId
    successCriteria:
    - condition: $statusCode == 200
    outputs:
      realmRoles: $response.body
    onSuccess:
    - name: noRoleMappings
      type: goto
      stepId: listGroupMemberships
      criteria:
      - context: $response.body
        condition: $.length == 0
        type: jsonpath
  - stepId: stripRoleMappings
    description: >-
      Revoke every realm-level role captured in the previous step by passing the
      role representations back to the delete-mappings endpoint.
    operationId: deleteUserRealmRoleMappings
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: userId
      in: path
      value: $steps.resolveUser.outputs.userId
    requestBody:
      contentType: application/json
      payload: $steps.listRoleMappings.outputs.realmRoles
    successCriteria:
    - condition: $statusCode == 204
  - stepId: listGroupMemberships
    description: >-
      Read the groups the user belongs to, since group membership carries roles
      of its own and survives a disabled account.
    operationId: getUserGroups
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: userId
      in: path
      value: $steps.resolveUser.outputs.userId
    successCriteria:
    - condition: $statusCode == 200
    outputs:
      groups: $response.body
      primaryGroupId: $response.body#/0/id
    onSuccess:
    - name: noGroupMemberships
      type: goto
      stepId: evaluateDeletion
      criteria:
      - context: $response.body
        condition: $.length == 0
        type: jsonpath
  - stepId: removeFromPrimaryGroup
    description: >-
      Remove the user from the first group returned. Keycloak exposes group
      removal one group at a time, so a user in several groups needs this
      workflow re-run per group id.
    operationId: removeUserFromGroup
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: userId
      in: path
      value: $steps.resolveUser.outputs.userId
    - name: groupId
      in: path
      value: $steps.listGroupMemberships.outputs.primaryGroupId
    successCriteria:
    - condition: $statusCode == 204
  - stepId: evaluateDeletion
    description: >-
      Re-read the account to confirm it is disabled, and stop here unless the
      caller explicitly asked for a hard delete.
    operationId: getUser
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: userId
      in: path
      value: $steps.resolveUser.outputs.userId
    successCriteria:
    - condition: $statusCode == 200
    outputs:
      finalEnabled: $response.body#/enabled
    onSuccess:
    - name: retainDisabledAccount
      type: end
      criteria:
      - condition: $inputs.hardDelete == false
  - stepId: deleteUserAccount
    description: >-
      Permanently delete the stripped account. Only reached when hardDelete is
      true; this cannot be undone.
    operationId: deleteUser
    parameters:
    - name: realm
      in: path
      value: $inputs.realm
    - name: userId
      in: path
      value: $steps.resolveUser.outputs.userId
    successCriteria:
    - condition: $statusCode == 204
  outputs:
    userId: $steps.resolveUser.outputs.userId
    username: $steps.resolveUser.outputs.resolvedUsername
    revokedRealmRoles: $steps.listRoleMappings.outputs.realmRoles
    formerGroups: $steps.listGroupMemberships.outputs.groups
    accountEnabled: $steps.evaluateDeletion.outputs.finalEnabled