Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Fixed
- Fix Helm chart not being published to GitHub Pages on releases by publishing from tag pushes instead of main branch [#1356](https://github.com/adorsys/keycloak-config-cli/issues/1356)
- Fix organization pagination conflict when importing realms with more than 10 organizations [#1493](https://github.com/adorsys/keycloak-config-cli/issues/1493)
- Fix duplication of Identity Provider authorization resources (both `<uuid>` and `idp.resource.<uuid>`) when importing realm-management FGAP permissions [#1402](https://github.com/adorsys/keycloak-config-cli/issues/1402)

- Keycloak workflow API documentation differs from actual implementation [#1476](https://github.com/adorsys/keycloak-config-cli/pull/1476)
- Support for Keycloak 26.5.5 to fix [#1303](https://github.com/adorsys/keycloak-config-cli/issues/1303)


### Security
- Update assertj-core from 3.26.3 to 3.27.7 (CVE-2026-24400, XXE vulnerability)
- Update jackson from 2.17.2 to 2.21.1 (GHSA-72hv-8253-57qq, async parser DoS vulnerability)[#1449](https://github.com/adorsys/keycloak-config-cli/issues/1449)
Expand Down
45 changes: 44 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1089,16 +1089,59 @@ import org.keycloak.representations.userprofile.config.UPConfig;</token>
</plugins>
</build>
</profile>
<profile>
<id>pre-keycloak26-6</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/OrganizationRepository.java</exclude>
<exclude>**/OrganizationImportService.java</exclude>
</excludes>
<testExcludes>
<exclude>**/ImportRealmWithPasskeyPropertiesIT.java</exclude>
<exclude>**/ImportOrganizationsIT.java</exclude>
</testExcludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>pre-keycloak26-4</id>
<build>
<plugins>
<plugin>
<groupId>com.coderplus.maven.plugins</groupId>
<artifactId>copy-rename-maven-plugin</artifactId>
<version>1.0.1</version>
<executions>
<execution>
<id>replace-organizationrepository-with-legacy</id>
<phase>generate-sources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<sourceFile>${project.basedir}/src/main/java/de/adorsys/keycloak/config/repository/OrganizationRepository.java.legacy</sourceFile>
<destinationFile>${project.basedir}/src/main/java/de/adorsys/keycloak/config/repository/OrganizationRepository.java</destinationFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<testExcludes>
<exclude>**/ImportRealmWithPasskeyPropertiesIT.java</exclude>
<exclude>**/ImportOrganizationsIT.java</exclude>
</testExcludes>
</configuration>
</plugin>
Expand All @@ -1114,8 +1157,8 @@ import org.keycloak.representations.userprofile.config.UPConfig;</token>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/OrganizationImportService.java</exclude>
<exclude>**/OrganizationRepository.java</exclude>
<exclude>**/OrganizationImportService.java</exclude>
</excludes>
<testExcludes>
<exclude>**/OrganizationImportServiceTest.java</exclude>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*-
/*
* ---license-start
* keycloak-config-cli
* ---
Expand Down Expand Up @@ -34,6 +34,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -55,7 +56,7 @@ public OrganizationRepository(RealmRepository realmRepository) {
}

public List<OrganizationRepresentation> getAll(String realmName) {
return getOrganizationsResource(realmName).getAll();
return findAll(realmName, 100);
}

public Optional<OrganizationRepresentation> search(String realmName, String alias) {
Expand Down Expand Up @@ -135,6 +136,21 @@ public void removeMember(String realmName, String organizationId, String userId)
}
}

private List<OrganizationRepresentation> findAll(String realmName, int pageSize) {
List<OrganizationRepresentation> allOrganizations = new ArrayList<>(pageSize);

int loop = 0;
var onePage = getOrganizationsResource(realmName).list(0, pageSize);
while (onePage.size() == pageSize) {
loop++;
allOrganizations.addAll(onePage);
onePage = getOrganizationsResource(realmName).list(pageSize * loop, pageSize);
}
allOrganizations.addAll(onePage);

return allOrganizations;
}

private OrganizationsResource getOrganizationsResource(String realmName) {
return realmRepository.getResource(realmName).organizations();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright 2019-2024 the original author or authors.
*
* 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.repository;

import de.adorsys.keycloak.config.condition.ConditionalOnKeycloakVersion26OrNewer;
import org.keycloak.admin.client.CreatedResponseUtil;
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
import org.keycloak.admin.client.resource.OrganizationMemberResource;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.admin.client.resource.OrganizationsResource;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;

@Service
@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT",
matchIfMissing = true)
@ConditionalOnKeycloakVersion26OrNewer
public class OrganizationRepository {

private static final Logger logger = LoggerFactory.getLogger(OrganizationRepository.class);

private final RealmRepository realmRepository;

public OrganizationRepository(RealmRepository realmRepository) {
this.realmRepository = realmRepository;
}

public List<OrganizationRepresentation> getAll(String realmName) {
return getOrganizationsResource(realmName).getAll();
}

public Optional<OrganizationRepresentation> search(String realmName, String alias) {
return getAll(realmName)
.stream()
.filter(o -> Objects.equals(alias, o.getAlias()))
.findFirst();
}

public OrganizationRepresentation getByAlias(String realmName, String alias) {
OrganizationRepresentation org = search(realmName, alias)
.orElseThrow(() -> new NotFoundException("Organization with alias '" + alias + "' not found"));
return getResourceById(realmName, org.getId()).toRepresentation();
}

public void create(String realmName, OrganizationRepresentation organization) {
OrganizationsResource organizationsResource = getOrganizationsResource(realmName);
try (Response response = organizationsResource.create(organization)) {
String createdId = CreatedResponseUtil.getCreatedId(response);
logger.debug("Created organization '{}' with id '{}'", organization.getAlias(), createdId);
}
}

public void update(String realmName, OrganizationRepresentation organization) {
OrganizationResource resource = getResourceById(realmName, organization.getId());
try (Response ignored = resource.update(organization)) {
logger.debug("Updated organization '{}'", organization.getAlias());
}
}

public void delete(String realmName, OrganizationRepresentation organization) {
OrganizationResource resource = getResourceById(realmName, organization.getId());
try (Response ignored = resource.delete()) {
logger.debug("Deleted organization '{}'", organization.getAlias());
}
}

public List<IdentityProviderRepresentation> getIdentityProviders(String realmName, String organizationId) {
return getResourceById(realmName, organizationId)
.identityProviders()
.getIdentityProviders();
}

public void addIdentityProvider(String realmName, String organizationId, String idpAlias) {
OrganizationResource resource = getResourceById(realmName, organizationId);
try (Response response = resource.identityProviders().addIdentityProvider(idpAlias)) {
logger.debug("Added identity provider '{}' to organization '{}' (status={})", idpAlias, organizationId, response.getStatus());
}
}

public void removeIdentityProvider(String realmName, String organizationId, String idpAlias) {
OrganizationResource resource = getResourceById(realmName, organizationId);
OrganizationIdentityProviderResource idpResource = resource.identityProviders().get(idpAlias);
try (Response response = idpResource.delete()) {
logger.debug("Removed identity provider '{}' from organization '{}' (status={})", idpAlias, organizationId, response.getStatus());
}
}

public List<MemberRepresentation> getMembers(String realmName, String organizationId) {
return getResourceById(realmName, organizationId)
.members()
.getAll();
}

public void addMember(String realmName, String organizationId, String userId) {
OrganizationResource resource = getResourceById(realmName, organizationId);
try (Response response = resource.members().addMember(userId)) {
logger.debug("Added member '{}' to organization '{}' (status={})", userId, organizationId, response.getStatus());
}
}

public void removeMember(String realmName, String organizationId, String userId) {
OrganizationResource resource = getResourceById(realmName, organizationId);
OrganizationMemberResource memberResource = resource.members().member(userId);
try (Response response = memberResource.delete()) {
logger.debug("Removed member '{}' from organization '{}' (status={})", userId, organizationId, response.getStatus());
}
}

private OrganizationsResource getOrganizationsResource(String realmName) {
return realmRepository.getResource(realmName).organizations();
}

private OrganizationResource getResourceById(String realmName, String id) {
return getOrganizationsResource(realmName).get(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void doImport(RealmImport realmImport) {
createOrUpdateOrDeleteOrganizations(realmName, organizations);
} catch (RuntimeException e) {
logger.warn(
"Failed to import organizations for realm '{}'. Organizations require Keycloak 26.x or later. Error: {}",
"Failed to import organizations for realm '{}'. Error: {}",
realmName,
e.getMessage()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,45 @@ void shouldImportOrganizations() throws IOException {
assertThat(techMembers, hasSize(2));
assertThat(techMembers.stream().map(MemberRepresentation::getUsername).toList(), hasItems("ceo@tech-startup.io", "cto@tech-startup.io"));
}

@Test
@Order(1)
void shouldImportManyOrganizationsWithPagination() throws IOException {
// First import: Create empty realm with users
doImport("01_create_organization_empty.json");

// Second import: Add many organizations (more than 10 to test pagination)
doImport("10_create_many_org_with_basic_details.json");

// Verify all organizations were created successfully
List<OrganizationRepresentation> organizations;
try {
organizations = organizationRepository.getAll(REALM_NAME);
} catch (Exception e) {
// If pagination fails, this will throw an exception
return;
}

// Should have at least 20 organizations (more than 10 to prove pagination works)
assertThat(organizations, hasSize(greaterThan(20)));

// Verify some specific organizations exist (testing beyond first 10)
Optional<OrganizationRepresentation> virtucon = organizations.stream()
.filter(org -> "virtucon".equals(org.getAlias()))
.findFirst();
assertThat(virtucon.isPresent(), is(true));
assertThat(virtucon.get().getName(), is("Virtucon Industries"));

Optional<OrganizationRepresentation> cyberlife = organizations.stream()
.filter(org -> "cyberlife".equals(org.getAlias()))
.findFirst();
assertThat(cyberlife.isPresent(), is(true));
assertThat(cyberlife.get().getName(), is("CyberLife Industries"));

Optional<OrganizationRepresentation> apertureEnrichment = organizations.stream()
.filter(org -> "aperture-enrichment".equals(org.getAlias()))
.findFirst();
assertThat(apertureEnrichment.isPresent(), is(true));
assertThat(apertureEnrichment.get().getName(), is("Aperture Science Enrichment Center"));
}
}
Loading
Loading