From 318bc01b4764d8e6366fac25a2171ddfc831b3fa Mon Sep 17 00:00:00 2001 From: opdt Date: Wed, 6 Nov 2024 14:49:00 +0100 Subject: [PATCH] chore(kc26): add identity provider storage provider Signed-off-by: opdt --- .github/workflows/ci.yaml | 4 +- .github/workflows/release.yaml | 2 +- CONTRIBUTING.md | 2 +- README.md | 6 +- core/pom.xml | 2 +- .../cassandra/CassandraDatastoreProvider.java | 10 +- .../cassandra/CassandraJsonSerialization.java | 24 +- .../client/CassandraClientAdapter.java | 35 +-- .../CassandraClientScopeAdapter.java | 25 +- ...ultCassandraConnectionProviderFactory.java | 2 + .../group/CassandraGroupProvider.java | 7 + .../persistence/entities/GroupValue.java | 8 + ...sandraIdentityProviderStorageProvider.java | 231 ++++++++++++++++++ ...dentityProviderStorageProviderFactory.java | 43 ++++ .../realm/CassandraRealmAdapter.java | 52 ++-- .../persistence/CassandraUserRepository.java | 6 +- .../DisabledStickySessionEncoderProvider.java | 5 + ...llInfinispanConnectionProviderFactory.java | 18 ++ ...usInfinispanConnectionProviderFactory.java | 18 ++ metrics/pom.xml | 2 +- pom.xml | 6 +- tests/pom.xml | 3 +- .../cassandra/testsuite/parameters/Map.java | 3 + 23 files changed, 402 insertions(+), 112 deletions(-) create mode 100644 core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProvider.java create mode 100644 core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProviderFactory.java diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95ab5657..5d68e9df 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,14 +18,14 @@ jobs: - uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'adopt' - name: Run tests run: mvn test - name: Create Jacoco report - run: mvn -f ./core org.jacoco:jacoco-maven-plugin:0.8.8:report -Djacoco.dataFile="$(readlink -f ./tests/target/jacoco.exec)" + run: mvn -f ./core org.jacoco:jacoco-maven-plugin:report -Djacoco.dataFile="$(readlink -f ./tests/target/jacoco.exec)" - name: Cache SonarCloud packages uses: actions/cache@v3 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6ba30c56..916111ef 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,7 @@ jobs: - name: Set up Maven Central Repository uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'adopt' server-id: ossrh server-username: MAVEN_USERNAME diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28accc79..103dc23d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Keycloak Cassandra is a datastore and caching extension for Keycloak, the Open S ## Building and working with the codebase -To build the codebase you need an installed JDK of at least version 17 and Maven. +To build the codebase you need an installed JDK of at least version 21 and Maven. The tests use the Testcontainers-framework to start a local Apache Cassandra database instance. For this to work, you need Docker installed on your system as well. ## Contributing to Keycloak Cassandra diff --git a/README.md b/README.md index 0e1f3707..e7ff7c59 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Cassandra storage extension for Keycloak Uses Apache Cassandra to store and retrieve entities of all storage areas except authorization and events. -Requires Keycloak >= 25.0.0 (older versions may be supported by older versions of this extension). +Requires Keycloak >= 26.0.0 (older versions may be supported by older versions of this extension). ## How to use @@ -39,6 +39,10 @@ The following parameters might be needed in addition to the configuration option ## Deviations from standard storage providers +### Organizations + +Keycloak Organizations are currently *not* supported and have to be turned off. + ### User Lookup Due to Cassandras query first nature, users can only be looked up by specific fields. `UserProvider::searchForUserStream` supports the following subset of Keycloaks standard search attributes: diff --git a/core/pom.xml b/core/pom.xml index 6038782e..9279005e 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -7,7 +7,7 @@ de.arbeitsagentur.opdt keycloak-cassandra-extension-parent - 4.0.6-25.0-SNAPSHOT + 4.0.6-26.0-SNAPSHOT ../pom.xml diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraDatastoreProvider.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraDatastoreProvider.java index 64443f0e..33754a8b 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraDatastoreProvider.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraDatastoreProvider.java @@ -15,11 +15,8 @@ */ package de.arbeitsagentur.opdt.keycloak.cassandra; -import java.util.HashSet; -import java.util.Set; import lombok.extern.jbosslog.JBossLog; import org.keycloak.models.*; -import org.keycloak.provider.Provider; import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.storage.ExportImportManager; import org.keycloak.storage.MigrationManager; @@ -30,8 +27,6 @@ public class CassandraDatastoreProvider extends DefaultDatastoreProvider { private final KeycloakSession session; - private final Set providersToClose = new HashSet<>(); - public CassandraDatastoreProvider(KeycloakSession session) { super(null, session); this.session = session; @@ -87,6 +82,11 @@ public UserSessionProvider userSessions() { return session.getProvider(UserSessionProvider.class); } + @Override + public IdentityProviderStorageProvider identityProviders() { + return session.getProvider(IdentityProviderStorageProvider.class); + } + @Override public ExportImportManager getExportImportManager() { return new CassandraLegacyExportImportManager(session); diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraJsonSerialization.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraJsonSerialization.java index a2196ac8..ccbaf260 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraJsonSerialization.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/CassandraJsonSerialization.java @@ -37,15 +37,27 @@ public static ObjectMapper getMapper() { return mapper; } - public static String writeValueAsString(Object obj) throws IOException { - return mapper.writeValueAsString(obj); + public static String writeValueAsString(Object obj) { + try { + return mapper.writeValueAsString(obj); + } catch (IOException e) { + throw new RuntimeException(e); + } } - public static T readValue(String bytes, Class type) throws IOException { - return mapper.readValue(bytes, type); + public static T readValue(String bytes, Class type) { + try { + return mapper.readValue(bytes, type); + } catch (IOException e) { + throw new RuntimeException(e); + } } - public static T readValue(String string, TypeReference type) throws IOException { - return mapper.readValue(string, type); + public static T readValue(String string, TypeReference type) { + try { + return mapper.readValue(string, type); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/client/CassandraClientAdapter.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/client/CassandraClientAdapter.java index 2feceb7f..b89ba4f8 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/client/CassandraClientAdapter.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/client/CassandraClientAdapter.java @@ -22,7 +22,6 @@ import de.arbeitsagentur.opdt.keycloak.cassandra.client.persistence.ClientRepository; import de.arbeitsagentur.opdt.keycloak.cassandra.client.persistence.entities.Client; import de.arbeitsagentur.opdt.keycloak.cassandra.transaction.TransactionalModelAdapter; -import java.io.IOException; import java.security.MessageDigest; import java.util.*; import java.util.function.Function; @@ -773,16 +772,7 @@ private void setSerializedAttributeValues(String name, List values) { List attributeValues = values.stream() .filter(Objects::nonNull) - .map( - value -> { - try { - return CassandraJsonSerialization.writeValueAsString(value); - } catch (IOException e) { - log.errorf( - "Cannot serialize %s (realm: %s, name: %s)", value, entity.getId(), name); - throw new RuntimeException(e); - } - }) + .map(CassandraJsonSerialization::writeValueAsString) .collect(Collectors.toCollection(ArrayList::new)); entity.getAttributes().put(name, attributeValues); @@ -800,16 +790,7 @@ private List getDeserializedAttributes(String name, TypeReference type } return values.stream() - .map( - value -> { - try { - return CassandraJsonSerialization.readValue(value, type); - } catch (IOException e) { - log.errorf( - "Cannot deserialize %s (realm: %s, name: %s)", value, entity.getId(), name); - throw new RuntimeException(e); - } - }) + .map(value -> CassandraJsonSerialization.readValue(value, type)) .filter(Objects::nonNull) .collect(Collectors.toCollection(ArrayList::new)); } @@ -822,17 +803,7 @@ private List getDeserializedAttributes(String name, Class type) { } return values.stream() - .map( - value -> { - try { - return CassandraJsonSerialization.readValue(value, type); - } catch (IOException e) { - log.errorf( - "Cannot deserialize %s (realm: %s, name: %s, type: %s)", - value, entity.getId(), name, type.getName()); - throw new RuntimeException(e); - } - }) + .map(value -> CassandraJsonSerialization.readValue(value, type)) .filter(Objects::nonNull) .collect(Collectors.toCollection(ArrayList::new)); } diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/clientScope/CassandraClientScopeAdapter.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/clientScope/CassandraClientScopeAdapter.java index d0ef17e0..8e66ba9c 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/clientScope/CassandraClientScopeAdapter.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/clientScope/CassandraClientScopeAdapter.java @@ -21,7 +21,6 @@ import de.arbeitsagentur.opdt.keycloak.cassandra.clientScope.persistence.ClientScopeRepository; import de.arbeitsagentur.opdt.keycloak.cassandra.clientScope.persistence.entities.ClientScopeValue; import de.arbeitsagentur.opdt.keycloak.cassandra.clientScope.persistence.entities.ClientScopes; -import java.io.IOException; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -261,17 +260,7 @@ private List getAttributeValues(String name) { private void setSerializedAttributeValues(String name, List values) { List attributeValues = values.stream() - .map( - value -> { - try { - return CassandraJsonSerialization.writeValueAsString(value); - } catch (IOException e) { - log.errorf( - "Cannot serialize %s (realm: %s, name: %s)", - value, clientScopeEntity.getId(), name); - throw new RuntimeException(e); - } - }) + .map(CassandraJsonSerialization::writeValueAsString) .collect(Collectors.toList()); clientScopeEntity.getAttributes().put(name, attributeValues); @@ -282,17 +271,7 @@ private List getDeserializedAttributes(String name, Class type) { List values = clientScopeEntity.getAttributes().getOrDefault(name, new ArrayList<>()); return values.stream() - .map( - value -> { - try { - return CassandraJsonSerialization.readValue(value, type); - } catch (IOException e) { - log.errorf( - "Cannot deserialize %s (realm: %s, name: %s, type: %s)", - value, clientScopeEntity.getId(), name, type.getName()); - throw new RuntimeException(e); - } - }) + .map(value -> CassandraJsonSerialization.readValue(value, type)) .collect(Collectors.toCollection(ArrayList::new)); } } diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/connection/DefaultCassandraConnectionProviderFactory.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/connection/DefaultCassandraConnectionProviderFactory.java index 1be2aa27..2c3dd574 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/connection/DefaultCassandraConnectionProviderFactory.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/connection/DefaultCassandraConnectionProviderFactory.java @@ -84,6 +84,7 @@ import org.cognitor.cassandra.migration.MigrationRepository; import org.cognitor.cassandra.migration.MigrationTask; import org.keycloak.Config; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserSessionModel; @@ -178,6 +179,7 @@ public void init(Config.Scope scope) { .withLocalDatacenter(localDatacenter) .withKeyspace(keyspace) .addTypeCodecs(new EnumNameCodec<>(UserSessionModel.State.class)) + .addTypeCodecs(new EnumNameCodec<>(GroupModel.Type.class)) .addTypeCodecs(new EnumNameCodec<>(UserSessionModel.SessionPersistenceState.class)) .addTypeCodecs(new EnumNameCodec<>(CommonClientSessionModel.ExecutionStatus.class)) .addTypeCodecs(new JsonCodec<>(RoleValue.class, CassandraJsonSerialization.getMapper())) diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/CassandraGroupProvider.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/CassandraGroupProvider.java index 5fcaad55..cf6f585d 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/CassandraGroupProvider.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/CassandraGroupProvider.java @@ -71,6 +71,12 @@ private Function entityToAdapterFunc(RealmModel realm) { @Override public GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) { + return createGroup(realm, id, GroupModel.Type.REALM, name, toParent); + } + + @Override + public GroupModel createGroup( + RealmModel realm, String id, GroupModel.Type type, String name, GroupModel toParent) { log.debugv( "createGroup(%s, %s, %s, %s)", realm.getId(), id, name, toParent == null ? "null" : toParent.getId()); @@ -90,6 +96,7 @@ public GroupModel createGroup(RealmModel realm, String id, String name, GroupMod .id(id == null ? KeycloakModelUtils.generateId() : id) .name(name) .parentId(toParent == null ? null : toParent.getId()) + .type(type) .build(); groups.addRealmGroup(group); diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/persistence/entities/GroupValue.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/persistence/entities/GroupValue.java index 759f390f..951bae2b 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/persistence/entities/GroupValue.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/group/persistence/entities/GroupValue.java @@ -1,7 +1,9 @@ package de.arbeitsagentur.opdt.keycloak.cassandra.group.persistence.entities; +import com.fasterxml.jackson.annotation.JsonSetter; import java.util.*; import lombok.*; +import org.keycloak.models.GroupModel; @AllArgsConstructor @NoArgsConstructor @@ -13,9 +15,15 @@ public class GroupValue { private String name; private String parentId; private String realmId; + @Builder.Default private GroupModel.Type type = GroupModel.Type.REALM; @Builder.Default private Map> attributes = new HashMap<>(); @Builder.Default private Set grantedRoles = new HashSet<>(); + @JsonSetter("type") + public void setType(GroupModel.Type type) { + this.type = type == null ? GroupModel.Type.REALM : type; + } + public Map> getAttributes() { if (attributes == null) { attributes = new HashMap<>(); diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProvider.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProvider.java new file mode 100644 index 00000000..f03e2a94 --- /dev/null +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProvider.java @@ -0,0 +1,231 @@ +package de.arbeitsagentur.opdt.keycloak.cassandra.identityProvider; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; +import lombok.extern.jbosslog.JBossLog; +import org.keycloak.models.*; +import org.keycloak.utils.StringUtil; + +@JBossLog +public class CassandraIdentityProviderStorageProvider implements IdentityProviderStorageProvider { + private final KeycloakSession session; + + public CassandraIdentityProviderStorageProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public IdentityProviderModel create(IdentityProviderModel model) { + getRealm().addIdentityProvider(model); + return getRealm().getIdentityProviderByAlias(model.getAlias()); + } + + @Override + public void update(IdentityProviderModel model) { + getRealm().updateIdentityProvider(model); + } + + @Override + public boolean remove(String providerAlias) { + getRealm().removeIdentityProviderByAlias(providerAlias); + return true; + } + + @Override + public void removeAll() { + getRealm() + .getIdentityProvidersStream() + .forEach(idp -> getRealm().removeIdentityProviderByAlias(idp.getAlias())); + } + + @Override + public IdentityProviderModel getById(String internalId) { + return getRealm() + .getIdentityProvidersStream() + .filter(idp -> idp.getInternalId().equals(internalId)) + .findFirst() + .orElse(null); + } + + @Override + public IdentityProviderModel getByAlias(String alias) { + return getRealm().getIdentityProviderByAlias(alias); + } + + @Override + public Stream getAllStream( + Map options, Integer firstResult, Integer maxResults) { + int first = firstResult == null || firstResult < 0 ? 0 : firstResult; + int resultCount = maxResults == null || maxResults < 0 ? Integer.MAX_VALUE : maxResults; + + return getRealm() + .getIdentityProvidersStream() + .filter( + idp -> { + if (options == null || options.isEmpty()) { + return true; + } + + if (options.containsKey(IdentityProviderModel.ORGANIZATION_ID)) { + String organizationId = options.get(IdentityProviderModel.ORGANIZATION_ID); + if (!Objects.equals(idp.getOrganizationId(), organizationId)) { + return false; + } + } + + if (options.containsKey(IdentityProviderModel.ORGANIZATION_ID_NOT_NULL)) { + if (idp.getOrganizationId() == null) { + return false; + } + } + + if (options.containsKey(IdentityProviderModel.ENABLED)) { + boolean enabled = Boolean.parseBoolean(options.get(IdentityProviderModel.ENABLED)); + if (idp.isEnabled() != enabled) { + return false; + } + } + + if (options.containsKey(IdentityProviderModel.HIDE_ON_LOGIN)) { + boolean hideOnLogin = + Boolean.parseBoolean(options.get(IdentityProviderModel.HIDE_ON_LOGIN)); + if (idp.isHideOnLogin() != hideOnLogin) { + return false; + } + } + + if (options.containsKey(IdentityProviderModel.LINK_ONLY)) { + boolean linkOnly = + Boolean.parseBoolean(options.get(IdentityProviderModel.LINK_ONLY)); + if (idp.isLinkOnly() != linkOnly) { + return false; + } + } + + if (options.containsKey(IdentityProviderModel.ALIAS)) { + String alias = options.get(IdentityProviderModel.ALIAS); + if (!idp.getAlias().equals(alias)) { + return false; + } + } + + if (options.containsKey(IdentityProviderModel.ALIAS_NOT_IN)) { + String aliasNotIn = options.get(IdentityProviderModel.ALIAS_NOT_IN); + if (Arrays.stream(aliasNotIn.split(",")) + .anyMatch(alias -> idp.getAlias().equals(alias))) { + return false; + } + } + + if (options.containsKey(IdentityProviderModel.SEARCH)) { + String search = options.get(IdentityProviderModel.SEARCH); + if (!StringUtil.isNullOrEmpty(search) && !idp.getAlias().contains(search)) { + return false; + } + } + + return true; + }) + .sorted(Comparator.comparing(IdentityProviderModel::getAlias)) + .skip(first) + .limit(resultCount); + } + + @Override + public Stream getByFlow( + String flowId, String search, Integer firstResult, Integer maxResults) { + int first = firstResult == null || firstResult < 0 ? 0 : firstResult; + int resultCount = maxResults == null || maxResults < 0 ? Integer.MAX_VALUE : maxResults; + + return getRealm() + .getIdentityProvidersStream() + .filter(idp -> search == null || idp.getAlias().contains(search.replace("*", ""))) + .filter( + idp -> + Objects.equals(idp.getFirstBrokerLoginFlowId(), flowId) + || Objects.equals(idp.getPostBrokerLoginFlowId(), flowId)) + .sorted(Comparator.comparing(IdentityProviderModel::getAlias)) + .skip(first) + .limit(resultCount) + .map(IdentityProviderModel::getAlias); + } + + @Override + public long count() { + return getRealm().getIdentityProvidersStream().count(); + } + + @Override + public IdentityProviderMapperModel createMapper(IdentityProviderMapperModel model) { + return getRealm().addIdentityProviderMapper(model); + } + + @Override + public void updateMapper(IdentityProviderMapperModel model) { + createMapper(model); + } + + @Override + public boolean removeMapper(IdentityProviderMapperModel model) { + getRealm().removeIdentityProviderMapper(model); + return true; + } + + @Override + public void removeAllMappers() { + getRealm().getIdentityProviderMappersStream().forEach(getRealm()::removeIdentityProviderMapper); + } + + @Override + public IdentityProviderMapperModel getMapperById(String id) { + return getRealm().getIdentityProviderMapperById(id); + } + + @Override + public IdentityProviderMapperModel getMapperByName(String identityProviderAlias, String name) { + return getRealm().getIdentityProviderMapperByName(identityProviderAlias, name); + } + + @Override + public Stream getMappersStream( + Map options, Integer firstResult, Integer maxResults) { + int first = firstResult == null || firstResult < 0 ? 0 : firstResult; + int resultCount = maxResults == null || maxResults < 0 ? Integer.MAX_VALUE : maxResults; + + return getRealm() + .getIdentityProviderMappersStream() + .filter( + idp -> { + if (options == null || options.isEmpty()) { + return true; + } + + return idp.getConfig().entrySet().containsAll(options.entrySet()); + }) + .sorted(Comparator.comparing(IdentityProviderMapperModel::getName)) + .skip(first) + .limit(resultCount); + } + + @Override + public Stream getMappersByAliasStream(String identityProviderAlias) { + return getRealm() + .getIdentityProviderMappersStream() + .filter(mapper -> mapper.getIdentityProviderAlias().equals(identityProviderAlias)) + .sorted(Comparator.comparing(IdentityProviderMapperModel::getName)); + } + + @Override + public void close() {} + + private RealmModel getRealm() { + RealmModel realm = session.getContext().getRealm(); + if (realm == null) { + throw new IllegalStateException("Session not bound to a realm"); + } + return realm; + } +} diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProviderFactory.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProviderFactory.java new file mode 100644 index 00000000..063169ad --- /dev/null +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/identityProvider/CassandraIdentityProviderStorageProviderFactory.java @@ -0,0 +1,43 @@ +package de.arbeitsagentur.opdt.keycloak.cassandra.identityProvider; + +import static de.arbeitsagentur.opdt.keycloak.common.CommunityProfiles.isCassandraProfileEnabled; +import static org.keycloak.userprofile.DeclarativeUserProfileProviderFactory.PROVIDER_PRIORITY; + +import com.google.auto.service.AutoService; +import org.keycloak.Config; +import org.keycloak.models.*; +import org.keycloak.provider.EnvironmentDependentProviderFactory; + +@AutoService(IdentityProviderStorageProviderFactory.class) +public class CassandraIdentityProviderStorageProviderFactory + implements IdentityProviderStorageProviderFactory, + EnvironmentDependentProviderFactory { + @Override + public boolean isSupported(Config.Scope scope) { + return isCassandraProfileEnabled(); + } + + @Override + public CassandraIdentityProviderStorageProvider create(KeycloakSession session) { + return new CassandraIdentityProviderStorageProvider(session); + } + + @Override + public void init(Config.Scope config) {} + + @Override + public void postInit(KeycloakSessionFactory factory) {} + + @Override + public void close() {} + + @Override + public String getId() { + return "cassandra"; + } + + @Override + public int order() { + return PROVIDER_PRIORITY + 1; + } +} diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/realm/CassandraRealmAdapter.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/realm/CassandraRealmAdapter.java index 76aa660f..30eafb2f 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/realm/CassandraRealmAdapter.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/realm/CassandraRealmAdapter.java @@ -25,7 +25,6 @@ import de.arbeitsagentur.opdt.keycloak.cassandra.realm.persistence.entities.Realm; import de.arbeitsagentur.opdt.keycloak.cassandra.transaction.TransactionalModelAdapter; import de.arbeitsagentur.opdt.keycloak.common.TimeAdapter; -import java.io.IOException; import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -41,6 +40,7 @@ import org.keycloak.models.*; import org.keycloak.models.utils.ComponentUtil; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.idm.RealmRepresentation; @EqualsAndHashCode(callSuper = true) @JBossLog @@ -213,6 +213,9 @@ public class CassandraRealmAdapter extends TransactionalModelAdapter impl public static final String IS_ORGANIZATIONS_ENABLED = AttributeTypes.INTERNAL_ATTRIBUTE_PREFIX + "organizationsEnabled"; + public static final String BRUTE_FORCE_STRATEGY = + AttributeTypes.INTERNAL_ATTRIBUTE_PREFIX + "bruteForceStrategy"; + @EqualsAndHashCode.Exclude private final KeycloakSession session; @EqualsAndHashCode.Exclude private final RealmRepository realmRepository; @@ -373,6 +376,19 @@ public void setMaxTemporaryLockouts(int value) { setAttribute(MAX_TEMPORARY_LOCKOUTS, value); } + @Override + public RealmRepresentation.BruteForceStrategy getBruteForceStrategy() { + String strategy = getAttribute(BRUTE_FORCE_STRATEGY); + return strategy == null + ? RealmRepresentation.BruteForceStrategy.MULTIPLE + : RealmRepresentation.BruteForceStrategy.valueOf(strategy); + } + + @Override + public void setBruteForceStrategy(RealmRepresentation.BruteForceStrategy val) { + setAttribute(BRUTE_FORCE_STRATEGY, val == null ? null : val.name()); + } + @Override public int getMaxFailureWaitSeconds() { return getAttribute(MAX_FAILURE_WAIT_SECONDS, 0); @@ -1864,16 +1880,7 @@ private void setSerializedAttributeValue(String name, T value) { private void setSerializedAttributeValues(String name, List values) { List attributeValues = values.stream() - .map( - value -> { - try { - return CassandraJsonSerialization.writeValueAsString(value); - } catch (IOException e) { - log.errorf( - "Cannot serialize %s (realm: %s, name: %s)", value, entity.getId(), name); - throw new RuntimeException(e); - } - }) + .map(CassandraJsonSerialization::writeValueAsString) .collect(Collectors.toCollection(ArrayList::new)); entity.getAttributes().put(name, attributeValues); @@ -1895,16 +1902,7 @@ private List getDeserializedAttributes(String name, TypeReference type } return values.stream() - .map( - value -> { - try { - return CassandraJsonSerialization.readValue(value, type); - } catch (IOException e) { - log.errorf( - "Cannot deserialize %s (realm: %s, name: %s)", value, entity.getId(), name); - throw new RuntimeException(e); - } - }) + .map(value -> CassandraJsonSerialization.readValue(value, type)) .collect(Collectors.toCollection(ArrayList::new)); } @@ -1912,17 +1910,7 @@ private List getDeserializedAttributes(String name, Class type) { List values = entity.getAttribute(name); return values.stream() - .map( - value -> { - try { - return CassandraJsonSerialization.readValue(value, type); - } catch (IOException e) { - log.errorf( - "Cannot deserialize %s (realm: %s, name: %s, type: %s)", - value, entity.getId(), name, type.getName()); - throw new RuntimeException(e); - } - }) + .map(value -> CassandraJsonSerialization.readValue(value, type)) .collect(Collectors.toCollection(ArrayList::new)); } diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/user/persistence/CassandraUserRepository.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/user/persistence/CassandraUserRepository.java index 14f641bd..801131c3 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/user/persistence/CassandraUserRepository.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/cassandra/user/persistence/CassandraUserRepository.java @@ -72,7 +72,7 @@ public User findUserByEmail(String realmId, String email) { return null; } - return users.get(0); + return users.getFirst(); } @Override @@ -96,7 +96,7 @@ public User findUserByUsername(String realmId, String username) { return null; } - return users.get(0); + return users.getFirst(); } @Override @@ -120,7 +120,7 @@ public User findUserByUsernameCaseInsensitive(String realmId, String username) { return null; } - return users.get(0); + return users.getFirst(); } @Override diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/DisabledStickySessionEncoderProvider.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/DisabledStickySessionEncoderProvider.java index 12cde569..a8dc2c6d 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/DisabledStickySessionEncoderProvider.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/DisabledStickySessionEncoderProvider.java @@ -90,4 +90,9 @@ public boolean isSupported(Config.Scope config) { public Map getOperationalInfo() { return Map.of("implementation", "disabled (cassandra-extension)"); } + + @Override + public void setShouldAttachRoute(boolean b) { + // do nothing + } } diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullInfinispanConnectionProviderFactory.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullInfinispanConnectionProviderFactory.java index 095cbc84..5d231709 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullInfinispanConnectionProviderFactory.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullInfinispanConnectionProviderFactory.java @@ -23,9 +23,12 @@ import com.google.auto.service.AutoService; import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ScheduledExecutorService; import lombok.extern.jbosslog.JBossLog; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.util.concurrent.BlockingManager; import org.keycloak.Config; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory; @@ -73,6 +76,21 @@ public TopologyInfo getTopologyInfo() { return null; } + @Override + public CompletionStage migrateToProtoStream() { + return null; + } + + @Override + public ScheduledExecutorService getScheduledExecutor() { + return null; + } + + @Override + public BlockingManager getBlockingManager() { + return null; + } + @Override public void close() {} }); diff --git a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullQuarkusInfinispanConnectionProviderFactory.java b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullQuarkusInfinispanConnectionProviderFactory.java index 86370081..7b650035 100644 --- a/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullQuarkusInfinispanConnectionProviderFactory.java +++ b/core/src/main/java/de/arbeitsagentur/opdt/keycloak/compatibility/NullQuarkusInfinispanConnectionProviderFactory.java @@ -23,9 +23,12 @@ import com.google.auto.service.AutoService; import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ScheduledExecutorService; import lombok.extern.jbosslog.JBossLog; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.util.concurrent.BlockingManager; import org.keycloak.Config; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory; @@ -73,6 +76,21 @@ public TopologyInfo getTopologyInfo() { return null; } + @Override + public CompletionStage migrateToProtoStream() { + return null; + } + + @Override + public ScheduledExecutorService getScheduledExecutor() { + return null; + } + + @Override + public BlockingManager getBlockingManager() { + return null; + } + @Override public void close() {} }); diff --git a/metrics/pom.xml b/metrics/pom.xml index 841200ff..f73b89ea 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -7,7 +7,7 @@ de.arbeitsagentur.opdt keycloak-cassandra-extension-parent - 4.0.6-25.0-SNAPSHOT + 4.0.6-26.0-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 26e519d1..25337bc0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.arbeitsagentur.opdt keycloak-cassandra-extension-parent - 4.0.6-25.0-SNAPSHOT + 4.0.6-26.0-SNAPSHOT pom https://github.com/opdt/keycloak-cassandra-extension @@ -42,12 +42,12 @@ UTF-8 - 17 + 21 ${java.version} ${java.version} - 25.0.6 + 26.0.6 3.22.0 diff --git a/tests/pom.xml b/tests/pom.xml index 930f2a58..ced8ab27 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -7,7 +7,7 @@ de.arbeitsagentur.opdt keycloak-cassandra-extension-parent - 4.0.6-25.0-SNAPSHOT + 4.0.6-26.0-SNAPSHOT ../pom.xml @@ -163,6 +163,7 @@ file:${project.build.directory}/test-classes/log4j.properties disabled + disabled org.jboss.logmanager.LogManager log4j -Djava.awt.headless=true ${surefire.memory.settings} ${surefire.system.args} diff --git a/tests/src/test/java/de/arbeitsagentur/opdt/keycloak/cassandra/testsuite/parameters/Map.java b/tests/src/test/java/de/arbeitsagentur/opdt/keycloak/cassandra/testsuite/parameters/Map.java index 0f6814e5..d89b7656 100644 --- a/tests/src/test/java/de/arbeitsagentur/opdt/keycloak/cassandra/testsuite/parameters/Map.java +++ b/tests/src/test/java/de/arbeitsagentur/opdt/keycloak/cassandra/testsuite/parameters/Map.java @@ -21,6 +21,7 @@ import de.arbeitsagentur.opdt.keycloak.cassandra.client.CassandraClientProviderFactory; import de.arbeitsagentur.opdt.keycloak.cassandra.clientScope.CassandraClientScopeProviderFactory; import de.arbeitsagentur.opdt.keycloak.cassandra.group.CassandraGroupProviderFactory; +import de.arbeitsagentur.opdt.keycloak.cassandra.identityProvider.CassandraIdentityProviderStorageProviderFactory; import de.arbeitsagentur.opdt.keycloak.cassandra.loginFailure.CassandraLoginFailureProviderFactory; import de.arbeitsagentur.opdt.keycloak.cassandra.realm.CassandraRealmsProviderFactory; import de.arbeitsagentur.opdt.keycloak.cassandra.role.CassandraRoleProviderFactory; @@ -75,6 +76,7 @@ public class Map extends KeycloakModelParameters { .add(DeviceRepresentationSpi.class) .add(UserProfileSpi.class) .add(ValidatorSPI.class) + .add(IdentityProviderStorageSpi.class) .build(); static final Set> ALLOWED_FACTORIES = @@ -89,6 +91,7 @@ public class Map extends KeycloakModelParameters { .add(CassandraUserProviderFactory.class) .add(CassandraUserSessionProviderFactory.class) .add(CassandraLoginFailureProviderFactory.class) + .add(CassandraIdentityProviderStorageProviderFactory.class) .add(SingleUseObjectProviderFactory.class) .add(TransientPublicKeyStorageProviderFactory.class) .add(DefaultClientPolicyManagerFactory.class)