From 7b89d151c1cc716912084ebf66399f992b73a6d2 Mon Sep 17 00:00:00 2001 From: vramik Date: Mon, 20 Dec 2021 15:05:01 +0100 Subject: [PATCH] KEYCLOAK-18565 JPA roles no-downtime store --- .../models/map/storage/jpa/Constants.java | 1 + .../jpa/JpaMapStorageProviderFactory.java | 8 +- .../storage/jpa/JpaModelCriteriaBuilder.java | 102 ++++++ .../client/JpaClientModelCriteriaBuilder.java | 62 +--- .../delegate/JpaClientDelegateProvider.java | 4 +- .../jpa/client/entity/JpaClientEntity.java | 5 + .../hibernate/jsonb/JpaEntityMigration.java | 4 + .../jpa/hibernate/jsonb/JsonbType.java | 3 + .../jsonb/migration/JpaRoleMigration.java | 30 ++ .../role/JpaRoleMapKeycloakTransaction.java | 160 ++++++++++ .../jpa/role/JpaRoleModelCriteriaBuilder.java | 131 ++++++++ .../delegate/JpaRoleDelegateProvider.java | 74 +++++ .../role/entity/JpaRoleAttributeEntity.java | 103 ++++++ .../jpa/role/entity/JpaRoleEntity.java | 301 ++++++++++++++++++ .../jpa/role/entity/JpaRoleMetadata.java | 43 +++ .../clients/jpa-clients-changelog-1.xml | 2 + .../META-INF/jpa-roles-changelog.xml | 23 ++ .../main/resources/META-INF/persistence.xml | 4 + .../META-INF/roles/jpa-roles-changelog-1.xml | 81 +++++ .../models/map/role/MapRoleEntity.java | 10 +- .../models/map/role/MapRoleProvider.java | 8 +- .../map/storage/chm/MapFieldPredicates.java | 1 - .../java/org/keycloak/models/RoleModel.java | 1 - 23 files changed, 1090 insertions(+), 71 deletions(-) create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaModelCriteriaBuilder.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaRoleMigration.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapKeycloakTransaction.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleAttributeEntity.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleMetadata.java create mode 100644 model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml create mode 100644 model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java index bdb5a5db5c..325ab36788 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java @@ -18,4 +18,5 @@ package org.keycloak.models.map.storage.jpa; public interface Constants { public static final Integer SUPPORTED_VERSION_CLIENT = 1; + public static final Integer SUPPORTED_VERSION_ROLE = 1; } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java index 11ce54059a..a16c385be0 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java @@ -42,6 +42,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RoleModel; import org.keycloak.models.dblock.DBLockProvider; import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; @@ -50,6 +51,8 @@ import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.models.map.storage.jpa.client.JpaClientMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.role.JpaRoleMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider; import static org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider.Status.VALID; import org.keycloak.models.utils.KeycloakModelUtils; @@ -70,11 +73,14 @@ public class JpaMapStorageProviderFactory implements //client .constructor(JpaClientEntity.class, JpaClientEntity::new) .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) + //role + .constructor(JpaRoleEntity.class, JpaRoleEntity::new) .build(); private static final Map, Function> MODEL_TO_TX = new HashMap<>(); static { - MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new); + MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new); + MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new); } public MapKeycloakTransaction createTransaction(Class modelType, EntityManager em) { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaModelCriteriaBuilder.java new file mode 100644 index 0000000000..896de6180c --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaModelCriteriaBuilder.java @@ -0,0 +1,102 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import org.keycloak.models.map.storage.CriterionNotSupportedException; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; +import org.keycloak.storage.SearchableModelField; + +/** + * Abstract class containing methods common to all Jpa*ModelCriteriaBuilder implementations + * + * @param Entity + * @param Model + * @param specific implementation of this class + */ +public abstract class JpaModelCriteriaBuilder> implements ModelCriteriaBuilder { + + private final Function, Predicate>, Self> instantiator; + private BiFunction, Predicate> predicateFunc = null; + + public JpaModelCriteriaBuilder(Function, Predicate>, Self> instantiator) { + this.instantiator = instantiator; + } + + public JpaModelCriteriaBuilder(Function, Predicate>, Self> instantiator, + BiFunction, Predicate> predicateFunc) { + this.instantiator = instantiator; + this.predicateFunc = predicateFunc; + } + + protected void validateValue(Object[] value, SearchableModelField field, ModelCriteriaBuilder.Operator op, Class... expectedTypes) { + if (value == null || expectedTypes == null || value.length != expectedTypes.length) { + throw new CriterionNotSupportedException(field, op, "Invalid argument: " + Arrays.toString(value)); + } + for (int i = 0; i < expectedTypes.length; i++) { + if (! expectedTypes[i].isInstance(value[i])) { + throw new CriterionNotSupportedException(field, op, "Expected types: " + Arrays.toString(expectedTypes) + + " but got: " + Arrays.toString(value)); + } + } + } + + protected String convertToJson(Object input) { + try { + return JsonbType.MAPPER.writeValueAsString(input); + } catch (JsonProcessingException ex) { + throw new RuntimeException("Unable to write value as String.", ex); + } + } + + @Override + public Self and(Self... builders) { + return instantiator.apply((cb, root) -> cb.and(Stream.of(builders).map((Self b) -> { + if ( !(b instanceof JpaModelCriteriaBuilder)) throw new IllegalStateException("Invalid type of ModelCriteriaBuilder."); + return ((JpaModelCriteriaBuilder) b).getPredicateFunc().apply(cb, root); + }).toArray(Predicate[]::new))); + } + + @Override + public Self or(Self... builders) { + return instantiator.apply((cb, root) -> cb.or(Stream.of(builders).map((Self b) -> { + if ( !(b instanceof JpaModelCriteriaBuilder)) throw new IllegalStateException("Invalid type of ModelCriteriaBuilder."); + return ((JpaModelCriteriaBuilder) b).getPredicateFunc().apply(cb, root); + }).toArray(Predicate[]::new))); + } + + @Override + public Self not(Self builder) { + return instantiator.apply((cb, root) -> { + if ( !(builder instanceof JpaModelCriteriaBuilder)) throw new IllegalStateException("Invalid type of ModelCriteriaBuilder."); + BiFunction, Predicate> predFunc = ((JpaModelCriteriaBuilder) builder).getPredicateFunc(); + return cb.not(predFunc.apply(cb, root)); + }); + } + + public BiFunction, Predicate> getPredicateFunc() { + return predicateFunc; + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java index 0a364a5fb8..f9091a978c 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java @@ -16,12 +16,7 @@ */ package org.keycloak.models.map.storage.jpa.client; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; -import org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity; -import java.util.Arrays; import java.util.function.BiFunction; -import java.util.stream.Stream; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; @@ -30,38 +25,19 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel.SearchableFields; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.models.map.storage.CriterionNotSupportedException; -import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity; +import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.storage.SearchableModelField; -public class JpaClientModelCriteriaBuilder implements ModelCriteriaBuilder { - - private BiFunction, Predicate> predicateFunc = null; +public class JpaClientModelCriteriaBuilder extends JpaModelCriteriaBuilder { public JpaClientModelCriteriaBuilder() { + super(JpaClientModelCriteriaBuilder::new); } private JpaClientModelCriteriaBuilder(BiFunction, Predicate> predicateFunc) { - this.predicateFunc = predicateFunc; - } - - private void validateValue(Object[] value, SearchableModelField field, Operator op, Class... expectedTypes) { - if (value == null || expectedTypes == null || value.length != expectedTypes.length) { - throw new CriterionNotSupportedException(field, op, "Invalid argument: " + Arrays.toString(value)); - } - for (int i = 0; i < expectedTypes.length; i++) { - if (! expectedTypes[i].isInstance(value[i])) { - throw new CriterionNotSupportedException(field, op, "Expected types: " + Arrays.toString(expectedTypes) + - " but got: " + Arrays.toString(value)); - } - } - } - - private String convertToJson(Object input) { - try { - return JsonbType.MAPPER.writeValueAsString(input); - } catch (JsonProcessingException ex) { - throw new RuntimeException("Unable to write value as String.", ex); - } + super(JpaClientModelCriteriaBuilder::new, predicateFunc); } @Override @@ -125,30 +101,4 @@ public class JpaClientModelCriteriaBuilder implements ModelCriteriaBuilder cb.and(Stream.of(builders).map(b -> b.getPredicateFunc().apply(cb, root)).toArray(Predicate[]::new)) - ); - } - - @Override - public JpaClientModelCriteriaBuilder or(JpaClientModelCriteriaBuilder... builders) { - return new JpaClientModelCriteriaBuilder( - (cb, root) -> cb.or(Stream.of(builders).map(b -> b.getPredicateFunc().apply(cb, root)).toArray(Predicate[]::new)) - ); - } - - @Override - public JpaClientModelCriteriaBuilder not(JpaClientModelCriteriaBuilder builder) { - return new JpaClientModelCriteriaBuilder( - (cb, root) -> cb.not(builder.getPredicateFunc().apply(cb, root)) - ); - } - - BiFunction, Predicate> getPredicateFunc() { - return predicateFunc; - } - } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java index affd4ba7d3..7c400e4ec3 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java @@ -35,8 +35,8 @@ public class JpaClientDelegateProvider implements DelegateProvider, BiFunction> MIGRATIONS = new HashMap<>(); static { MIGRATIONS.put(JpaClientMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, SUPPORTED_VERSION_CLIENT, tree, JpaClientMigration.MIGRATORS)); + MIGRATIONS.put(JpaRoleMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, SUPPORTED_VERSION_ROLE, tree, JpaRoleMigration.MIGRATORS)); } private static ObjectNode migrateTreeTo(int entityVersion, Integer supportedVersion, ObjectNode node, List> migrators) { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java index 710c5da91c..0893fdf387 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java @@ -76,6 +76,9 @@ public class JsonbType extends AbstractSingleColumnStandardBasicType imp abstract class IgnoredMetadataFieldsMixIn { @JsonIgnore public abstract String getId(); @JsonIgnore public abstract Map> getAttributes(); + + // roles: assumed it's true when getClient() != null, see AbstractRoleEntity.isClientRole() + @JsonIgnore public abstract Boolean isClientRole(); } public JsonbType() { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaRoleMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaRoleMigration.java new file mode 100644 index 0000000000..330d0153ef --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaRoleMigration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +public class JpaRoleMigration { + + public static final List> MIGRATORS = Arrays.asList( + o -> o // no migration yet + ); + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapKeycloakTransaction.java new file mode 100644 index 0000000000..5029cf4fa3 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapKeycloakTransaction.java @@ -0,0 +1,160 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa.role; + +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaDelete; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Root; +import org.keycloak.connections.jpa.JpaKeycloakTransaction; +import org.keycloak.models.RoleModel; +import static org.keycloak.models.jpa.PaginationUtils.paginateQuery; +import org.keycloak.models.map.common.StringKeyConvertor.UUIDKey; +import org.keycloak.models.map.role.MapRoleEntity; +import org.keycloak.models.map.role.MapRoleEntityDelegate; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.QueryParameters; +import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_ROLE; +import static org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory.CLONER; +import org.keycloak.models.map.storage.jpa.role.delegate.JpaRoleDelegateProvider; +import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; +import static org.keycloak.utils.StreamsUtil.closing; + +public class JpaRoleMapKeycloakTransaction extends JpaKeycloakTransaction implements MapKeycloakTransaction { + + public JpaRoleMapKeycloakTransaction(EntityManager em) { + super(em); + } + + @Override + public MapRoleEntity create(MapRoleEntity mapEntity) { + JpaRoleEntity jpaEntity = (JpaRoleEntity) CLONER.from(mapEntity); + if (mapEntity.getId() == null) { + jpaEntity.setId(UUIDKey.INSTANCE.yieldNewUniqueKey().toString()); + } + jpaEntity.setEntityVersion(SUPPORTED_VERSION_ROLE); + em.persist(jpaEntity); + return jpaEntity; + } + + @Override + public MapRoleEntity read(String key) { + if (key == null) return null; + UUID uuid = UUIDKey.INSTANCE.fromStringSafe(key); + if (uuid == null) return null; + + return em.find(JpaRoleEntity.class, uuid); + } + + @Override + public Stream read(QueryParameters queryParameters) { + JpaRoleModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder() + .flashToModelCriteriaBuilder(new JpaRoleModelCriteriaBuilder()); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(JpaRoleEntity.class); + Root root = query.from(JpaRoleEntity.class); + query.select(cb.construct(JpaRoleEntity.class, + root.get("id"), + root.get("entityVersion"), + root.get("realmId"), + root.get("clientId"), + root.get("name"), + root.get("description") + )); + + if (!queryParameters.getOrderBy().isEmpty()) { + List orderByList = new LinkedList<>(); + for (QueryParameters.OrderBy order : queryParameters.getOrderBy()) { + switch (order.getOrder()) { + case ASCENDING: + orderByList.add(cb.asc(root.get(order.getModelField().getName()))); + break; + case DESCENDING: + orderByList.add(cb.desc(root.get(order.getModelField().getName()))); + break; + default: + throw new UnsupportedOperationException("Unknown ordering."); + } + } + query.orderBy(orderByList); + } + + if (mcb.getPredicateFunc() != null) query.where(mcb.getPredicateFunc().apply(cb, root)); + + return closing( + paginateQuery(em.createQuery(query), queryParameters.getOffset(), queryParameters.getLimit()) + .getResultStream()) + .map(r -> new MapRoleEntityDelegate(new JpaRoleDelegateProvider(r, em))); + } + + @Override + public long getCount(QueryParameters queryParameters) { + JpaRoleModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder() + .flashToModelCriteriaBuilder(new JpaRoleModelCriteriaBuilder()); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery countQuery = cb.createQuery(Long.class); + Root root = countQuery.from(JpaRoleEntity.class); + countQuery.select(cb.count(root)); + + if (mcb.getPredicateFunc() != null) countQuery.where(mcb.getPredicateFunc().apply(cb, root)); + + return em.createQuery(countQuery).getSingleResult(); + } + + @Override + public boolean delete(String key) { + if (key == null) return false; + UUID uuid = UUIDKey.INSTANCE.fromStringSafe(key); + if (uuid == null) return false; + em.remove(em.getReference(JpaRoleEntity.class, uuid)); + return true; + } + + @Override + public long delete(QueryParameters queryParameters) { + JpaRoleModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder() + .flashToModelCriteriaBuilder(new JpaRoleModelCriteriaBuilder()); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaDelete deleteQuery = cb.createCriteriaDelete(JpaRoleEntity.class); + + Root root = deleteQuery.from(JpaRoleEntity.class); + + if (mcb.getPredicateFunc() != null) deleteQuery.where(mcb.getPredicateFunc().apply(cb, root)); + +// TODO find out if the flush and clear are needed here or not, since delete(QueryParameters) +// is not used yet from the code it's difficult to investigate its potential purpose here +// according to https://thorben-janssen.com/5-common-hibernate-mistakes-that-cause-dozens-of-unexpected-queries/#Remove_Child_Entities_With_a_Bulk_Operation +// it seems it is necessary unless it is sure that any of removed entities wasn't fetched +// Once KEYCLOAK-19697 is done we could test our scenarios and see if we need the flush and clear +// em.flush(); +// em.clear(); + + return em.createQuery(deleteQuery).executeUpdate(); + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java new file mode 100644 index 0000000000..e79d7761ed --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java @@ -0,0 +1,131 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa.role; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaBuilder.In; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import org.keycloak.models.RoleModel; +import org.keycloak.models.RoleModel.SearchableFields; +import org.keycloak.models.map.common.StringKeyConvertor.UUIDKey; +import org.keycloak.models.map.storage.CriterionNotSupportedException; +import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; +import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; +import org.keycloak.storage.SearchableModelField; + +public class JpaRoleModelCriteriaBuilder extends JpaModelCriteriaBuilder { + + public JpaRoleModelCriteriaBuilder() { + super(JpaRoleModelCriteriaBuilder::new); + } + + private JpaRoleModelCriteriaBuilder(BiFunction, Predicate> predicateFunc) { + super(JpaRoleModelCriteriaBuilder::new, predicateFunc); + } + + @Override + @SuppressWarnings("unchecked") + public JpaRoleModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value) { + switch (op) { + case EQ: + if (modelField.equals(SearchableFields.REALM_ID) || + modelField.equals(SearchableFields.CLIENT_ID) || + modelField.equals(SearchableFields.NAME)) { + validateValue(value, modelField, op, String.class); + + return new JpaRoleModelCriteriaBuilder((cb, root) -> + cb.equal(root.get(modelField.getName()), value[0]) + ); + } else { + throw new CriterionNotSupportedException(modelField, op); + } + case NE: + if (modelField.equals(SearchableFields.IS_CLIENT_ROLE)) { + + validateValue(value, modelField, op, Boolean.class); + + return new JpaRoleModelCriteriaBuilder((cb, root) -> + ((Boolean) value[0]) ? cb.isNull(root.get("clientId")) : cb.isNotNull(root.get("clientId")) + ); + } else { + throw new CriterionNotSupportedException(modelField, op); + } + case IN: + if (modelField.equals(SearchableFields.ID)) { + if (value == null || value.length == 0) throw new CriterionNotSupportedException(modelField, op); + + final Collection collectionValues; + if (value.length == 1) { + + if (value[0] instanceof Object[]) { + collectionValues = Arrays.asList(value[0]); + } else if (value[0] instanceof Collection) { + collectionValues = (Collection) value[0]; + } else if (value[0] instanceof Stream) { + try (Stream str = ((Stream) value[0])) { + collectionValues = str.collect(Collectors.toCollection(ArrayList::new)); + } + } else { + collectionValues = Collections.singleton(value[0]); + } + + } else { + collectionValues = new HashSet(Arrays.asList(value)); + } + + return new JpaRoleModelCriteriaBuilder((cb, root) -> { + In in = cb.in(root.get("id")); + for (Object id : collectionValues) { + try { + in.value(UUIDKey.INSTANCE.fromString(Objects.toString(id, null))); + } catch (IllegalArgumentException e) { + throw new CriterionNotSupportedException(modelField, op, id + " id is not in uuid format.", e); + } + } + return in; + }); + } else { + throw new CriterionNotSupportedException(modelField, op); + } + case ILIKE: + if (modelField.equals(SearchableFields.NAME) || + modelField.equals(SearchableFields.DESCRIPTION)) { + + validateValue(value, modelField, op, String.class); + + return new JpaRoleModelCriteriaBuilder((cb, root) -> + cb.like(cb.lower(root.get(modelField.getName())), value[0].toString().toLowerCase()) + ); + } else { + throw new CriterionNotSupportedException(modelField, op); + } + default: + throw new CriterionNotSupportedException(modelField, op); + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java new file mode 100644 index 0000000000..41d5f6604c --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java @@ -0,0 +1,74 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa.role.delegate; + +import java.util.UUID; +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Root; +import org.keycloak.models.map.common.EntityField; +import org.keycloak.models.map.common.delegate.DelegateProvider; +import org.keycloak.models.map.role.MapRoleEntity; +import org.keycloak.models.map.role.MapRoleEntityFields; +import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; + +public class JpaRoleDelegateProvider implements DelegateProvider { + + private JpaRoleEntity delegate; + private final EntityManager em; + + public JpaRoleDelegateProvider(JpaRoleEntity delegate, EntityManager em) { + this.delegate = delegate; + this.em = em; + } + + @Override + public JpaRoleEntity getDelegate(boolean isRead, Enum> field, Object... parameters) { + if (delegate.isMetadataInitialized()) return delegate; + if (isRead) { + if (field instanceof MapRoleEntityFields) { + switch ((MapRoleEntityFields) field) { + case ID: + case REALM_ID: + case CLIENT_ID: + case NAME: + case DESCRIPTION: + return delegate; + + case ATTRIBUTES: + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(JpaRoleEntity.class); + Root root = query.from(JpaRoleEntity.class); + root.fetch("attributes", JoinType.INNER); + query.select(root).where(cb.equal(root.get("id"), UUID.fromString(delegate.getId()))); + + delegate = em.createQuery(query).getSingleResult(); + break; + + default: + delegate = em.find(JpaRoleEntity.class, UUID.fromString(delegate.getId())); + } + } else throw new IllegalStateException("Not a valid role field: " + field); + } else { + delegate = em.find(JpaRoleEntity.class, UUID.fromString(delegate.getId())); + } + return delegate; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleAttributeEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleAttributeEntity.java new file mode 100644 index 0000000000..f162bd321c --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleAttributeEntity.java @@ -0,0 +1,103 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa.role.entity; + +import java.io.Serializable; +import java.util.Objects; +import java.util.UUID; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import org.hibernate.annotations.Nationalized; + +@Entity +@Table(name = "role_attribute") +public class JpaRoleAttributeEntity implements Serializable { + + @Id + @Column + @GeneratedValue + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="fk_role") + private JpaRoleEntity role; + + @Column + private String name; + + @Nationalized + @Column + private String value; + + public JpaRoleAttributeEntity() { + } + + public JpaRoleAttributeEntity(JpaRoleEntity role, String name, String value) { + this.role = role; + this.name = name; + this.value = value; + } + + public UUID getId() { + return id; + } + + public JpaRoleEntity getRole() { + return role; + } + + public void setRole(JpaRoleEntity role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JpaRoleAttributeEntity)) return false; + JpaRoleAttributeEntity that = (JpaRoleAttributeEntity) obj; + return Objects.equals(getRole(), that.getRole()) && + Objects.equals(getName(), that.getName()) && + Objects.equals(getValue(), that.getValue()); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java new file mode 100644 index 0000000000..9f1812cec4 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java @@ -0,0 +1,301 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa.role.entity; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.role.MapRoleEntity.AbstractRoleEntity; +import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_ROLE; +import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; + +/** + * There are some fields marked by {@code @Column(insertable = false, updatable = false)}. + * Those fields are automatically generated by database from json field, + * therefore marked as non-insertable and non-updatable to instruct hibernate. + */ +@Entity +@Table(name = "role", uniqueConstraints = {@UniqueConstraint(columnNames = {"realmId", "clientId", "name"})}) +@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) +public class JpaRoleEntity extends AbstractRoleEntity implements Serializable { + + @Id + @Column + private UUID id; + + //used for implicit optimistic locking + @Version + @Column + private int version; + + @Type(type = "jsonb") + @Column(columnDefinition = "jsonb") + private final JpaRoleMetadata metadata; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private Integer entityVersion; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private String realmId; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private String clientId; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private String name; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private String description; + + @OneToMany(mappedBy = "role", cascade = CascadeType.PERSIST, orphanRemoval = true) + private final Set attributes = new HashSet<>(); + + /** + * No-argument constructor, used by hibernate to instantiate entities. + */ + public JpaRoleEntity() { + this.metadata = new JpaRoleMetadata(); + } + + public JpaRoleEntity(DeepCloner cloner) { + this.metadata = new JpaRoleMetadata(cloner); + } + + /** + * Used by hibernate when calling cb.construct from read(QueryParameters) method. + * It is used to select role without metadata(json) field. + */ + public JpaRoleEntity(UUID id, Integer entityVersion, String realmId, String clientId, String name, String description) { + this.id = id; + this.entityVersion = entityVersion; + this.realmId = realmId; + this.clientId = clientId; + this.name = name; + this.description = description; + this.metadata = null; + } + + public boolean isMetadataInitialized() { + return metadata != null; + } + + /** + * In case of any update on entity, we want to update the entityVerion + * to current one. + */ + private void checkEntityVersionForUpdate() { + Integer ev = getEntityVersion(); + if (ev != null && ev < SUPPORTED_VERSION_ROLE) { + setEntityVersion(SUPPORTED_VERSION_ROLE); + } + } + + public Integer getEntityVersion() { + if (isMetadataInitialized()) return metadata.getEntityVersion(); + return entityVersion; + } + + public void setEntityVersion(Integer entityVersion) { + metadata.setEntityVersion(entityVersion); + } + + public int getVersion() { + return version; + } + + @Override + public String getId() { + return id == null ? null : id.toString(); + } + + @Override + public void setId(String id) { + this.id = id == null ? null : UUID.fromString(id); + } + + @Override + public String getRealmId() { + if (isMetadataInitialized()) return metadata.getRealmId(); + return realmId; + } + + @Override + public String getClientId() { + if (isMetadataInitialized()) return metadata.getClientId(); + return clientId; + } + + @Override + public String getName() { + if (isMetadataInitialized()) return metadata.getName(); + return name; + } + + @Override + public String getDescription() { + if (isMetadataInitialized()) return metadata.getDescription(); + return description; + } + + @Override + public void setClientRole(Boolean clientRole) { + // intentionally do nothing, assuming the role is client-role when this.getClientId() != null; + } + + @Override + public void setRealmId(String realmId) { + checkEntityVersionForUpdate(); + metadata.setRealmId(realmId); + } + + @Override + public void setClientId(String clientId) { + checkEntityVersionForUpdate(); + metadata.setClientId(clientId); + } + + @Override + public void setName(String name) { + checkEntityVersionForUpdate(); + metadata.setName(name); + } + + @Override + public void setDescription(String description) { + checkEntityVersionForUpdate(); + metadata.setDescription(description); + } + + @Override + public Set getCompositeRoles() { + return metadata.getCompositeRoles(); + } + + @Override + public void setCompositeRoles(Set compositeRoles) { + checkEntityVersionForUpdate(); + metadata.setCompositeRoles(compositeRoles); + } + + @Override + public void addCompositeRole(String roleId) { + checkEntityVersionForUpdate(); + metadata.addCompositeRole(roleId); + } + + @Override + public void removeCompositeRole(String roleId) { + checkEntityVersionForUpdate(); + metadata.removeCompositeRole(roleId); + } + + @Override + public Map> getAttributes() { + Map> result = new HashMap<>(); + for (JpaRoleAttributeEntity attribute : attributes) { + List values = result.getOrDefault(attribute.getName(), new LinkedList<>()); + values.add(attribute.getValue()); + result.put(attribute.getName(), values); + } + return result; + } + + @Override + public List getAttribute(String name) { + return attributes.stream() + .filter(a -> Objects.equals(a.getName(), name)) + .map(JpaRoleAttributeEntity::getValue) + .collect(Collectors.toList()); + } + + @Override + public void setAttributes(Map> attributes) { + checkEntityVersionForUpdate(); + for (Iterator iterator = this.attributes.iterator(); iterator.hasNext();) { + JpaRoleAttributeEntity attr = iterator.next(); + iterator.remove(); + attr.setRole(null); + } + if (attributes != null) { + for (Map.Entry> entry : attributes.entrySet()) { + setAttribute(entry.getKey(), entry.getValue()); + } + } + } + + @Override + public void setAttribute(String name, List values) { + checkEntityVersionForUpdate(); + removeAttribute(name); + for (String value : values) { + JpaRoleAttributeEntity attribute = new JpaRoleAttributeEntity(this, name, value); + attributes.add(attribute); + } + } + + @Override + public void removeAttribute(String name) { + checkEntityVersionForUpdate(); + for (Iterator iterator = attributes.iterator(); iterator.hasNext();) { + JpaRoleAttributeEntity attr = iterator.next(); + if (Objects.equals(attr.getName(), name)) { + iterator.remove(); + attr.setRole(null); + } + } + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JpaRoleEntity)) return false; + return Objects.equals(getId(), ((JpaRoleEntity) obj).getId()); + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleMetadata.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleMetadata.java new file mode 100644 index 0000000000..a09b3252fd --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleMetadata.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ +package org.keycloak.models.map.storage.jpa.role.entity; + +import java.io.Serializable; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.role.MapRoleEntityImpl; + +public class JpaRoleMetadata extends MapRoleEntityImpl implements Serializable { + + public JpaRoleMetadata(DeepCloner cloner) { + super(cloner); + } + + public JpaRoleMetadata() { + super(); + } + + private Integer entityVersion; + + public Integer getEntityVersion() { + return entityVersion; + } + + public void setEntityVersion(Integer entityVersion) { + this.entityVersion = entityVersion; + } + +} diff --git a/model/map-jpa/src/main/resources/META-INF/clients/jpa-clients-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/clients/jpa-clients-changelog-1.xml index 4a48fe088b..7e262159d3 100644 --- a/model/map-jpa/src/main/resources/META-INF/clients/jpa-clients-changelog-1.xml +++ b/model/map-jpa/src/main/resources/META-INF/clients/jpa-clients-changelog-1.xml @@ -48,9 +48,11 @@ limitations under the License. + diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml new file mode 100644 index 0000000000..5c6efafaa6 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/model/map-jpa/src/main/resources/META-INF/persistence.xml b/model/map-jpa/src/main/resources/META-INF/persistence.xml index 46825ea809..44db64ca33 100644 --- a/model/map-jpa/src/main/resources/META-INF/persistence.xml +++ b/model/map-jpa/src/main/resources/META-INF/persistence.xml @@ -1,7 +1,11 @@ + org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity + + org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity + org.keycloak.models.map.storage.jpa.role.entity.JpaRoleAttributeEntity diff --git a/model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml new file mode 100644 index 0000000000..fe38c2735c --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java index 5c96aafce6..5f03030a6d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java @@ -16,8 +16,6 @@ */ package org.keycloak.models.map.role; -import java.util.List; -import java.util.Map; import java.util.Set; import org.keycloak.models.map.annotations.GenerateEntityImplementations; import org.keycloak.models.map.common.AbstractEntity; @@ -47,10 +45,10 @@ public interface MapRoleEntity extends AbstractEntity, UpdatableEntity, EntityWi this.updated |= id != null; } - } - - default Boolean isComposite() { - return ! (getCompositeRoles() == null || getCompositeRoles().isEmpty()); + @Override + public Boolean isClientRole() { + return getClientId() != null; + } } Boolean isClientRole(); diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java index 7c582e52a1..3d376bb5a9 100644 --- a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java @@ -61,7 +61,7 @@ public class MapRoleProvider implements RoleProvider { @Override public RoleModel addRealmRole(RealmModel realm, String id, String name) { if (getRealmRole(realm, name) != null) { - throw new ModelDuplicateException("Role exists: " + id); + throw new ModelDuplicateException("Role with the same name exists: " + name + " for realm " + realm.getName()); } LOG.tracef("addRealmRole(%s, %s, %s)%s", realm, id, name, getShortStackTrace()); @@ -82,7 +82,7 @@ public class MapRoleProvider implements RoleProvider { public Stream getRealmRolesStream(RealmModel realm, Integer first, Integer max) { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) - .compare(SearchableFields.IS_CLIENT_ROLE, Operator.NE, true); + .compare(SearchableFields.IS_CLIENT_ROLE, Operator.NE, true); return tx.read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); @@ -109,7 +109,7 @@ public class MapRoleProvider implements RoleProvider { public Stream getRealmRolesStream(RealmModel realm) { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) - .compare(SearchableFields.IS_CLIENT_ROLE, Operator.NE, true); + .compare(SearchableFields.IS_CLIENT_ROLE, Operator.NE, true); return tx.read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) .map(entityToAdapterFunc(realm)); @@ -118,7 +118,7 @@ public class MapRoleProvider implements RoleProvider { @Override public RoleModel addClientRole(ClientModel client, String id, String name) { if (getClientRole(client, name) != null) { - throw new ModelDuplicateException("Role exists: " + id); + throw new ModelDuplicateException("Role with the same name exists: " + name + " for client " + client.getClientId()); } LOG.tracef("addClientRole(%s, %s, %s)%s", client, id, name, getShortStackTrace()); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java index d5a3f19081..eda1098678 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java @@ -122,7 +122,6 @@ public class MapFieldPredicates { put(ROLE_PREDICATES, RoleModel.SearchableFields.DESCRIPTION, MapRoleEntity::getDescription); put(ROLE_PREDICATES, RoleModel.SearchableFields.NAME, MapRoleEntity::getName); put(ROLE_PREDICATES, RoleModel.SearchableFields.IS_CLIENT_ROLE, MapRoleEntity::isClientRole); - put(ROLE_PREDICATES, RoleModel.SearchableFields.IS_COMPOSITE_ROLE, MapRoleEntity::isComposite); put(USER_PREDICATES, UserModel.SearchableFields.REALM_ID, MapUserEntity::getRealmId); put(USER_PREDICATES, UserModel.SearchableFields.USERNAME, MapUserEntity::getUsername); diff --git a/server-spi/src/main/java/org/keycloak/models/RoleModel.java b/server-spi/src/main/java/org/keycloak/models/RoleModel.java index c802077f97..e0289a922a 100755 --- a/server-spi/src/main/java/org/keycloak/models/RoleModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleModel.java @@ -38,7 +38,6 @@ public interface RoleModel { public static final SearchableModelField NAME = new SearchableModelField<>("name", String.class); public static final SearchableModelField DESCRIPTION = new SearchableModelField<>("description", String.class); public static final SearchableModelField IS_CLIENT_ROLE = new SearchableModelField<>("isClientRole", Boolean.class); - public static final SearchableModelField IS_COMPOSITE_ROLE = new SearchableModelField<>("isCompositeRole", Boolean.class); } String getName();