Status: Implemented ✅
This document describes the YAML-based Keycloak configuration system used in the Operations Manager.
The Operations Manager provides a declarative YAML format for configuring Keycloak realms, clients, users, groups, and roles. This system combines:
- Custom orchestration logic - Variables, loops, output chaining, and control flow
- Keycloak API conventions - Standard Keycloak field names and structures
This hybrid approach allows you to:
- Define infrastructure as code
- Avoid repetitive configuration
- Maintain consistency across environments
- Use familiar Keycloak terminology
Processes YAML configurations and executes them via the Keycloak connector. The handler:
- Resolves variables using Jinja2-style
{{ variable }}syntax - Expands
forEachloops to create multiple resources from templates - Chains outputs between operations using the
as:keyword - Calls appropriate Keycloak connector methods for each section
Sections are processed in this order to handle dependencies:
realms:- Create or configure realmsidentityProviders:- Add SSO/OIDC identity providersauthenticationFlows:- Configure authentication flowsclientScopes:- Create custom client scopesplatformClients:- Create federation clients (for SSO)clients:- Create OIDC clientsrealmRoles:- Create realm-level rolesgroups:- Create groupsusers:- Create users with roles and group memberships
Define reusable data at the top of your YAML file:
variables:
demo_groups:
- name: "ICTU"
path: "/ICTU"
- name: "MIN_BZK"
path: "/MIN_BZK"
admin_roles:
- "admin"
- "all_groups"Reference variables using {{ variable_name }}:
realms:
- name: "{{ realm_name }}"
displayName: "{{ realm_display_name }}"Create multiple resources from a list:
groups:
forEach: "{{ demo_groups }}"
as: group
group:
name: "{{ group.name }}"
path: "{{ group.path }}"This creates one group for each item in demo_groups.
Capture results from one operation and use them in subsequent operations:
platformClients:
- as: platform_client # Capture output
clientId: "federation-client"
realm: "{{ platform_realm_name }}"
identityProviders:
- alias: "sso-provider"
config:
clientId: "{{ platform_client.client_id }}" # Use captured output
clientSecret: "{{ platform_client.client_secret }}"The as: keyword stores the operation result, making it available to later sections.
Inside each configuration item, use standard Keycloak field names:
realms:
- name: "my-realm"
displayName: "My Application Realm"
enabled: true
registrationAllowed: true
loginWithEmailAllowed: true
resetPasswordAllowed: true
verifyEmail: true
bruteForceProtected: true
loginTheme: "nl-design-system"clients:
- clientId: "authentication-client"
name: "My App - Authentication"
enabled: true
publicClient: true # Public client (no secret)
protocol: "openid-connect"
redirectUris:
- "http://localhost:3000/*"
webOrigins:
- "+"
standardFlowEnabled: true
directAccessGrantsEnabled: true
serviceAccountsEnabled: falseProtocol mappers use Keycloak's dotted-key convention:
protocolMappers:
- name: "app-realm-roles"
protocol: "openid-connect"
protocolMapper: "oidc-usermodel-realm-role-mapper"
config:
claim.name: "roles" # Dotted keys are Keycloak convention
jsonType.label: "String"
multivalued: "true"
id.token.claim: "true"
access.token.claim: "true"
userinfo.token.claim: "true"Note: The dots in keys like claim.name are part of the key name itself, not path separators. This is Keycloak's standard configuration format.
users:
- username: "admin"
enabled: true
email: "admin@example.com"
firstName: "Admin"
lastName: "User"
credentials:
- type: "password"
value: "secure-password"
temporary: false
realmRoles: # Assign realm roles
- "admin"
- "user"
groups: # Add to groups
- "Administrators"
removeDefaultGroups: true # Remove Keycloak's default group assignmentsFor confidential clients with service accounts:
clients:
- clientId: "service-client"
publicClient: false
serviceAccountsEnabled: true
authorizationServicesEnabled: true
serviceAccountClientRoles: # Assign roles to service account
realm-management:
- "manage-users"
- "view-users"
- "manage-clients"Create client roles and restrict access to users with specific roles:
clients:
- clientId: "restricted-app"
publicClient: true
redirectUris:
- "https://app.example.com/*"
# Define client roles
clientRoles:
- name: "allowed-user"
description: "Users allowed to access this application"
# Restrict access to users with the role
restrictAccess:
enabled: true
role: "allowed-user"
errorMessage: "${accessDeniedNoPermission}" # Theme message key in ${} formatThis creates a custom authentication flow that:
- Checks if the user has the specified client role
- Denies access with a custom error message if they don't
Note: Users must be manually assigned the role after their first SSO login. See Client Access Restriction for details.
A realm that only allows SSO authentication (no local users):
variables:
platform_realm: "rig-platform"
realms:
- name: "{{ project_realm_name }}"
displayName: "{{ project_name }}"
enabled: true
registrationAllowed: false # No local registration
loginWithEmailAllowed: false # No local login
platformClients:
- as: platform_client
realm: "{{ platform_realm }}"
clientId: "{{ project_name }}-federation"
redirectUris:
- "{{ keycloak_url }}/realms/{{ project_realm_name }}/broker/sso/endpoint/*"
identityProviders:
- alias: "sso-rijk"
displayName: "SSO Rijk"
providerId: "oidc"
authenticateByDefault: true # Auto-redirect to SSO
config:
clientId: "{{ platform_client.client_id }}"
clientSecret: "{{ platform_client.client_secret }}"
discoveryUrl: "{{ sso_discovery_url }}"A realm for an application with local authentication, custom roles, and organization groups:
variables:
organizations:
- name: "ICTU"
- name: "MIN_BZK"
admin_users:
- username: "admin1"
email: "admin1@example.com"
groups: ["ICTU"]
- username: "admin2"
email: "admin2@example.com"
groups: ["MIN_BZK"]
realms:
- name: "{{ realm_name }}"
enabled: true
registrationAllowed: true
loginWithEmailAllowed: true
realmRoles:
- name: "admin"
description: "Administrator role"
- name: "editor"
description: "Editor role"
- name: "viewer"
description: "Viewer role"
groups:
forEach: "{{ organizations }}"
as: org
group:
name: "{{ org.name }}"
path: "/{{ org.name }}"
clients:
- clientId: "web-app"
publicClient: true
redirectUris:
- "http://localhost:3000/*"
protocolMappers:
- name: "roles-mapper"
protocol: "openid-connect"
protocolMapper: "oidc-usermodel-realm-role-mapper"
config:
claim.name: "roles"
multivalued: "true"
id.token.claim: "true"
access.token.claim: "true"
users:
forEach: "{{ admin_users }}"
as: user
user:
username: "{{ user.username }}"
email: "{{ user.email }}"
credentials:
- type: "password"
value: "demo123"
temporary: false
realmRoles:
- "admin"
groups: "{{ user.groups }}"
removeDefaultGroups: trueKeycloak automatically assigns users to default groups. To have clean group memberships:
users:
- username: "user1"
removeDefaultGroups: true # Remove all default group assignments
groups:
- "CustomGroup" # Only assign specified groupsFor applications that need group names without path prefixes, use the group mapper:
protocolMappers:
- name: "app-groups"
protocol: "openid-connect"
protocolMapper: "oidc-group-membership-mapper"
config:
claim.name: "groups"
full.path: "false" # Groups appear as "ICTU" not "/ICTU"
id.token.claim: "true"
access.token.claim: "true"YAML configuration files are stored in:
operations-manager/python/opi/configs/keycloak/
├── bootstrap.yaml # RIG Platform realm setup
├── sso-only.yaml # Project realm with SSO-only auth
├── sso-support.yaml # Project realm with SSO + local auth
└── algoritmeregister.yaml # Application-specific realm
from pathlib import Path
from opi.handlers.keycloak_yaml_handler import KeycloakYamlHandler
from opi.connectors.keycloak import create_keycloak_connector
# Create connector
keycloak = await create_keycloak_connector()
# Build context with variables
context = {
"realm_name": "my-realm",
"realm_display_name": "My Application",
"keycloak_url": "https://keycloak.example.com",
}
# Execute configuration
yaml_path = Path("opi/configs/keycloak/my-config.yaml")
handler = KeycloakYamlHandler(keycloak)
await handler.execute_config(yaml_path, context)Context variables are provided by the calling code and can include:
realm_name- Target realm namerealm_display_name- Human-readable realm namekeycloak_url- Keycloak server URLproject_name- Project identifiercluster- Cluster name (production, development, local)- Any application-specific variables
The Keycloak connector provides methods for all operations:
Realm Operations:
create_realm()- Create/configure realmsrealm_exists()- Check if realm exists
Client Operations:
create_oidc_client()- Create generic OIDC clientscreate_deployment_client()- Create deployment-specific clientscreate_federation_client()- Create federation clients for SSOfind_client_by_client_id()- Find clients by ID
Identity Provider Operations:
add_identity_provider()- Add OIDC/SAML identity providerscreate_identity_provider_mapper()- Add attribute mappersensure_standard_oidc_mappers()- Add standard OIDC mappers
Client Scope Operations:
create_custom_client_scope()- Create client scopes with mappersassign_client_scope_as_realm_default()- Set as realm default
Authentication Flow Operations:
configure_sso_redirect_flow()- Configure SSO-only flows
User & Group Operations:
create_user()- Create usersassign_realm_roles_to_user()- Assign roles to usersjoin_user_to_group()- Add users to groupsleave_all_groups()- Remove user from all groups
Role & Group Operations:
create_realm_role()- Create realm rolescreate_group()- Create groups
The handler processes each section:
- Variable Substitution -
_substitute_variables()replaces{{ var }}placeholders - forEach Expansion -
_expand_foreach()expands loops into multiple items - Output Storage - Captures operation results using
as:keyword - Section Processing - Calls appropriate
_process_*()methods
Each section processor:
- Extracts configuration from YAML
- Substitutes variables
- Expands loops
- Calls connector methods
- Handles idempotency (409 Conflict errors)
- Create comprehensive JSON schemas for all configuration sections to enable validation
- Add schema validation to catch configuration errors early (before API calls)
- Document all available Keycloak field names and their meanings with examples
- Add examples for more complex scenarios (composite roles, fine-grained permissions, client roles)
- Create testing utilities for validating YAML configurations without applying them
- Add support for updating/patching existing configurations (not just creation/idempotent replace)
- Document all available protocol mapper types and their configurations with real-world use cases
- Create configuration templates for common use cases (SaaS app, internal tool, etc.)
- Add conditional sections (e.g.,
if: "{{ environment == 'production' }}") - Support configuration composition (import/extend other YAML files)
- Add dry-run mode to preview changes without applying them
- Create migration tools for converting existing Python bootstrap code to YAML
- Add rollback capability for failed deployments
- Document error handling and troubleshooting procedures
Last Updated: 2025-01-07 Status: Implemented and In Use