Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Add support for managing Organizations (Keycloak 26+ only) [#1286](https://github.com/adorsys/keycloak-config-cli/issues/1286)
- Create, update, and delete organizations with domains and attributes
- Link identity providers to organizations with advanced configuration:
- Domain-based email matching for automatic provider selection
- Auto-redirect when email domain matches organization domain
- Control identity provider visibility on login page
- Support for managed properties (full/no-delete modes)
- State tracking support: Only organizations created by config-cli are deleted in full managed mode
- Automatic normalization of organization data


## [6.4.0] - 2025-02-21
### Added
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,47 @@ The config files are based on the keycloak export files. You can use them to re-

[moped.json](./contrib/example-config/moped.json) is a full working example file you can consider. Other examples are located in the [test resources](./src/test/resources/import-files).

### Organizations (Keycloak 26+)

Organizations can be configured to support multi-tenancy:

```json
{
"realm": "my-realm",
"organizations": [
{
"alias": "my-org",
"name": "My Organization",
"enabled": true,
"description": "Organization description",
"redirectUrl": "https://my-org.example.com",
"domains": [
{"name": "example.com", "verified": true}
],
"attributes": {
"contactEmail": ["admin@example.com"]
},
"identityProviders": [
{
"alias": "corporate-saml",
"domain": "example.com",
"redirectWhenEmailMatches": true,
"hideOnLogin": false
}
]
}
]
}
```

**Identity Provider Configuration:**
- `alias`: The identity provider alias (required)
- `domain`: Email domain for automatic provider selection
- `redirectWhenEmailMatches`: Auto-redirect users with matching email domains
- `hideOnLogin`: Hide this provider on the login page

Note: Identity providers must be configured in the realm before linking them to organizations.

## Variable Substitution

keycloak-config-cli supports variable substitution of config files. This could be enabled by `import.var-substitution.enabled=true` (**disabled by default**).
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
- "8787:8787"
command:
- start-dev
- --features=admin-fine-grained-authz
- --features=admin-fine-grained-authz,organization
keycloak-legacy:
image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION}-legacy
environment:
Expand Down
73 changes: 73 additions & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
| Synchronize user profile | 5.4.0 | Synchronize the user profile configuration defined on the realm configuration |
| Synchronize client-policies | 5.6.0 | Synchronize the client-policies (clientProfiles and clientPolicies) while updating realms |
| Synchronize message bundles | 5.12.0 | Synchronize message bundles defined on the realm configuration |
| Add organizations | x.x.x | Add organizations with domains and identity provider links (Keycloak 26+ only) |
| Update organizations | x.x.x | Update organization properties, domains, and identity provider links (Keycloak 26+ only) |
| Remove organizations | x.x.x | Remove organizations while updating realms (Keycloak 26+ only) |
| Normalize realm exports | x.x.x | Normalize a full realm export to be more minimal |

# Specificities
Expand Down Expand Up @@ -87,6 +90,76 @@ So if you need this, you have to configure it like :
}
```

# Organizations (Keycloak 26+ only)

Organizations support multi-tenancy in Keycloak by grouping users and identity providers. This feature is only available in Keycloak 26 and later versions.

## Organization Configuration

```json
{
"organizations": [
{
"alias": "my-org",
"name": "My Organization",
"enabled": true,
"description": "Organization description",
"redirectUrl": "https://my-org.example.com",
"domains": [
{
"name": "my-org.com",
"verified": true
}
],
"attributes": {
"key": ["value1", "value2"]
},
"identityProviders": [
{
"alias": "saml-idp",
"domain": "my-org.com",
"redirectWhenEmailMatches": true,
"hideOnLogin": false
}
]
}
]
}
```

## Key Features:
- **Domains**: Organizations can have multiple domains with verification status
- **Identity Providers**: Link existing identity providers to organizations with configuration:
- `alias`: The identity provider alias (required)
- `domain`: Domain for email-based matching (optional)
- `redirectWhenEmailMatches`: Auto-redirect when email domain matches (optional, default: false)
- `hideOnLogin`: Hide this IdP on the login page (optional, default: false)
- **Attributes**: Support for multi-valued attributes
- **Managed Mode**: Organizations support full/no-delete managed modes
- **State Tracking**: When state tracking is enabled (default), only organizations created by config-cli are deleted in full managed mode. Manually created organizations are preserved.

## Identity Provider Configuration
When linking identity providers to organizations, configure each provider with:

```json
"identityProviders": [
{
"alias": "corporate-idp",
"domain": "corp.example.com",
"redirectWhenEmailMatches": true,
"hideOnLogin": true
}
]
```

## Important Notes:
- Organizations must be imported AFTER identity providers since they reference IdPs by alias
- The `enabled` field defaults to `true` if not specified
- Organization aliases are normalized (lowercase, spaces replaced with hyphens)
- Domain names are normalized to lowercase
- When `redirectWhenEmailMatches` is true, users with matching email domains are automatically redirected to the IdP
- Identity provider link configurations are managed declaratively - manual changes will be overwritten on next import

# User - initial password

To set an initial password that is only respect while the user is created, the userLabel must be named `initial`.
Expand Down
16 changes: 15 additions & 1 deletion docs/MANAGED.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ In some cases, it is required to include some Keycloak defaults because keycloak
| Authentication Flows | You have to copy the default components to your import JSON, except built-in flows.| `authentication-flow` |
| Identity Providers | - | `identity-provider` |
| Identity Provider Mappers | - | `identity-provider-mapper` |
| Clients | - | `client` |
| Clients | Only clients created by config-cli will be deleted (with state tracking). | `client` |
| Clients Authorization Resources | The 'Default Resource' is always included. | `client-authorization-resources` |
| Clients Authorization Policies | - | `client-authorization-policies` |
| Clients Authorization Scopes | - | `client-authorization-scopes` |
| Message Bundles | Only message bundles imported with config-cli will be managed/deleted. | `message-bundles` |
| Organizations | Only organizations created by config-cli will be deleted (with state tracking). Organizations with domains and identity provider links (Keycloak 26+ only). | `organization` |

### Disabling Deletion of Managed Entities

Expand All @@ -70,3 +71,16 @@ import.managed.required-action=no-delete
### State management

If `import.remote-state.enabled` is set to `true` (default value), keycloak-config-cli will purge only resources they created before by keycloak-config-cli. If `import.remote-state.enabled` is set to `false`, keycloak-config-cli will purge all existing entities if they are not defined in import json.

#### State-Tracked Resources

The following resources support state tracking:
- **Clients**: Only clients created by config-cli are deleted when using full managed mode
- **Organizations**: Only organizations created by config-cli are deleted when using full managed mode
- **Roles**: Realm and client roles created by config-cli
- **Components**: Components and sub-components created by config-cli
- **Required Actions**: Required actions created by config-cli
- **Client Authorization Resources**: Resources created by config-cli
- **Message Bundles**: Message bundles created by config-cli

This means that manually created resources (through Keycloak Admin UI or other means) will be preserved when state tracking is enabled.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*-
* ---license-start
* keycloak-config-cli
* ---
* Copyright (C) 2017 - 2025 adorsys GmbH & Co. KG @ https://adorsys.com
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*/

package de.adorsys.keycloak.config.model;

import java.util.Objects;

public class OrganizationDomainRepresentation {
private String name;
private Boolean verified;

public OrganizationDomainRepresentation() {
}

public OrganizationDomainRepresentation(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Boolean isVerified() {
return verified;
}

public void setVerified(Boolean verified) {
this.verified = verified;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrganizationDomainRepresentation that = (OrganizationDomainRepresentation) o;
return Objects.equals(name, that.name);
}

@Override
public int hashCode() {
return Objects.hash(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*-
* ---license-start
* keycloak-config-cli
* ---
* Copyright (C) 2017 - 2025 adorsys GmbH & Co. KG @ https://adorsys.com
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*/

package de.adorsys.keycloak.config.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Objects;

/**
* Configuration for linking an identity provider to an organization.
* Supports domain-based email matching and visibility settings.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OrganizationIdentityProviderConfig {

@JsonProperty("alias")
private String alias;

@JsonProperty("domain")
private String domain;

@JsonProperty("redirectWhenEmailMatches")
private Boolean redirectWhenEmailMatches;

@JsonProperty("hideOnLogin")
private Boolean hideOnLogin;

public OrganizationIdentityProviderConfig() {
}

public OrganizationIdentityProviderConfig(String alias) {
this.alias = alias;
}

public String getAlias() {
return alias;
}

public void setAlias(String alias) {
this.alias = alias;
}

public String getDomain() {
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

public Boolean getRedirectWhenEmailMatches() {
return redirectWhenEmailMatches;
}

public void setRedirectWhenEmailMatches(Boolean redirectWhenEmailMatches) {
this.redirectWhenEmailMatches = redirectWhenEmailMatches;
}

public Boolean getHideOnLogin() {
return hideOnLogin;
}

public void setHideOnLogin(Boolean hideOnLogin) {
this.hideOnLogin = hideOnLogin;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrganizationIdentityProviderConfig that = (OrganizationIdentityProviderConfig) o;
return Objects.equals(alias, that.alias);
}

@Override
public int hashCode() {
return Objects.hash(alias);
}

@Override
public String toString() {
return "OrganizationIdentityProviderConfig{"
+ "alias='" + alias + '\''
+ ", domain='" + domain + '\''
+ ", redirectWhenEmailMatches=" + redirectWhenEmailMatches
+ ", hideOnLogin=" + hideOnLogin
+ '}';
}
}
Loading
Loading