KEYCLOAK-18565 JPA roles no-downtime store

This commit is contained in:
vramik 2021-12-20 15:05:01 +01:00 committed by Hynek Mlnařík
parent 213b1f5042
commit 7b89d151c1
23 changed files with 1090 additions and 71 deletions

View file

@ -18,4 +18,5 @@ package org.keycloak.models.map.storage.jpa;
public interface Constants { public interface Constants {
public static final Integer SUPPORTED_VERSION_CLIENT = 1; public static final Integer SUPPORTED_VERSION_CLIENT = 1;
public static final Integer SUPPORTED_VERSION_ROLE = 1;
} }

View file

@ -42,6 +42,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RoleModel;
import org.keycloak.models.dblock.DBLockProvider; import org.keycloak.models.dblock.DBLockProvider;
import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.client.MapProtocolMapperEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; 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.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.models.map.storage.MapStorageProviderFactory;
import org.keycloak.models.map.storage.jpa.client.JpaClientMapKeycloakTransaction; 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 org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider;
import static org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider.Status.VALID; import static org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider.Status.VALID;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -70,11 +73,14 @@ public class JpaMapStorageProviderFactory implements
//client //client
.constructor(JpaClientEntity.class, JpaClientEntity::new) .constructor(JpaClientEntity.class, JpaClientEntity::new)
.constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
//role
.constructor(JpaRoleEntity.class, JpaRoleEntity::new)
.build(); .build();
private static final Map<Class<?>, Function<EntityManager, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>(); private static final Map<Class<?>, Function<EntityManager, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>();
static { 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) { public MapKeycloakTransaction createTransaction(Class<?> modelType, EntityManager em) {

View file

@ -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 <E> Entity
* @param <M> Model
* @param <Self> specific implementation of this class
*/
public abstract class JpaModelCriteriaBuilder<E, M, Self extends ModelCriteriaBuilder<M, Self>> implements ModelCriteriaBuilder<M, Self> {
private final Function<BiFunction<CriteriaBuilder, Root<E>, Predicate>, Self> instantiator;
private BiFunction<CriteriaBuilder, Root<E>, Predicate> predicateFunc = null;
public JpaModelCriteriaBuilder(Function<BiFunction<CriteriaBuilder, Root<E>, Predicate>, Self> instantiator) {
this.instantiator = instantiator;
}
public JpaModelCriteriaBuilder(Function<BiFunction<CriteriaBuilder, Root<E>, Predicate>, Self> instantiator,
BiFunction<CriteriaBuilder, Root<E>, 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<CriteriaBuilder, Root<E>, Predicate> predFunc = ((JpaModelCriteriaBuilder) builder).getPredicateFunc();
return cb.not(predFunc.apply(cb, root));
});
}
public BiFunction<CriteriaBuilder, Root<E>, Predicate> getPredicateFunc() {
return predicateFunc;
}
}

View file

@ -16,12 +16,7 @@
*/ */
package org.keycloak.models.map.storage.jpa.client; 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.function.BiFunction;
import java.util.stream.Stream;
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Join; import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
@ -30,38 +25,19 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientModel.SearchableFields; import org.keycloak.models.ClientModel.SearchableFields;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.storage.CriterionNotSupportedException; 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; import org.keycloak.storage.SearchableModelField;
public class JpaClientModelCriteriaBuilder implements ModelCriteriaBuilder<ClientModel, JpaClientModelCriteriaBuilder> { public class JpaClientModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaClientEntity, ClientModel, JpaClientModelCriteriaBuilder> {
private BiFunction<CriteriaBuilder, Root<JpaClientEntity>, Predicate> predicateFunc = null;
public JpaClientModelCriteriaBuilder() { public JpaClientModelCriteriaBuilder() {
super(JpaClientModelCriteriaBuilder::new);
} }
private JpaClientModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaClientEntity>, Predicate> predicateFunc) { private JpaClientModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaClientEntity>, Predicate> predicateFunc) {
this.predicateFunc = predicateFunc; super(JpaClientModelCriteriaBuilder::new, 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);
}
} }
@Override @Override
@ -125,30 +101,4 @@ public class JpaClientModelCriteriaBuilder implements ModelCriteriaBuilder<Clien
throw new CriterionNotSupportedException(modelField, op); throw new CriterionNotSupportedException(modelField, op);
} }
} }
@Override
public JpaClientModelCriteriaBuilder and(JpaClientModelCriteriaBuilder... builders) {
return new JpaClientModelCriteriaBuilder(
(cb, root) -> 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<CriteriaBuilder, Root<JpaClientEntity>, Predicate> getPredicateFunc() {
return predicateFunc;
}
} }

View file

@ -35,8 +35,8 @@ public class JpaClientDelegateProvider implements DelegateProvider<MapClientEnti
private JpaClientEntity delegate; private JpaClientEntity delegate;
private final EntityManager em; private final EntityManager em;
public JpaClientDelegateProvider(JpaClientEntity deledate, EntityManager em) { public JpaClientDelegateProvider(JpaClientEntity delegate, EntityManager em) {
this.delegate = deledate; this.delegate = delegate;
this.em = em; this.em = em;
} }

View file

@ -46,6 +46,11 @@ import org.keycloak.models.map.common.DeepCloner;
import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT; import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; 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 @Entity
@Table(name = "client") @Table(name = "client")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})

View file

@ -25,12 +25,16 @@ import java.util.function.Function;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientMetadata; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientMetadata;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaClientMigration; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaClientMigration;
import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT; import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT;
import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_ROLE;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaRoleMigration;
import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleMetadata;
public class JpaEntityMigration { public class JpaEntityMigration {
static final Map<Class<?>, BiFunction<ObjectNode, Integer, ObjectNode>> MIGRATIONS = new HashMap<>(); static final Map<Class<?>, BiFunction<ObjectNode, Integer, ObjectNode>> MIGRATIONS = new HashMap<>();
static { static {
MIGRATIONS.put(JpaClientMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, SUPPORTED_VERSION_CLIENT, tree, JpaClientMigration.MIGRATORS)); 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<Function<ObjectNode, ObjectNode>> migrators) { private static ObjectNode migrateTreeTo(int entityVersion, Integer supportedVersion, ObjectNode node, List<Function<ObjectNode, ObjectNode>> migrators) {

View file

@ -76,6 +76,9 @@ public class JsonbType extends AbstractSingleColumnStandardBasicType<Object> imp
abstract class IgnoredMetadataFieldsMixIn { abstract class IgnoredMetadataFieldsMixIn {
@JsonIgnore public abstract String getId(); @JsonIgnore public abstract String getId();
@JsonIgnore public abstract Map<String, List<String>> getAttributes(); @JsonIgnore public abstract Map<String, List<String>> getAttributes();
// roles: assumed it's true when getClient() != null, see AbstractRoleEntity.isClientRole()
@JsonIgnore public abstract Boolean isClientRole();
} }
public JsonbType() { public JsonbType() {

View file

@ -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<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
}

View file

@ -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<MapRoleEntity, RoleModel> {
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<MapRoleEntity> read(QueryParameters<RoleModel> queryParameters) {
JpaRoleModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaRoleModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaRoleEntity> query = cb.createQuery(JpaRoleEntity.class);
Root<JpaRoleEntity> 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<Order> orderByList = new LinkedList<>();
for (QueryParameters.OrderBy<RoleModel> 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<RoleModel> queryParameters) {
JpaRoleModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaRoleModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
Root<JpaRoleEntity> 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<RoleModel> queryParameters) {
JpaRoleModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaRoleModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaDelete<JpaRoleEntity> deleteQuery = cb.createCriteriaDelete(JpaRoleEntity.class);
Root<JpaRoleEntity> 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();
}
}

View file

@ -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<JpaRoleEntity, RoleModel, JpaRoleModelCriteriaBuilder> {
public JpaRoleModelCriteriaBuilder() {
super(JpaRoleModelCriteriaBuilder::new);
}
private JpaRoleModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaRoleEntity>, Predicate> predicateFunc) {
super(JpaRoleModelCriteriaBuilder::new, predicateFunc);
}
@Override
@SuppressWarnings("unchecked")
public JpaRoleModelCriteriaBuilder compare(SearchableModelField<? super RoleModel> 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<UUID> 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);
}
}
}

View file

@ -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<MapRoleEntity> {
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<? extends EntityField<MapRoleEntity>> 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<JpaRoleEntity> query = cb.createQuery(JpaRoleEntity.class);
Root<JpaRoleEntity> 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;
}
}

View file

@ -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());
}
}

View file

@ -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<JpaRoleAttributeEntity> 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<String> getCompositeRoles() {
return metadata.getCompositeRoles();
}
@Override
public void setCompositeRoles(Set<String> 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<String, List<String>> getAttributes() {
Map<String, List<String>> result = new HashMap<>();
for (JpaRoleAttributeEntity attribute : attributes) {
List<String> values = result.getOrDefault(attribute.getName(), new LinkedList<>());
values.add(attribute.getValue());
result.put(attribute.getName(), values);
}
return result;
}
@Override
public List<String> getAttribute(String name) {
return attributes.stream()
.filter(a -> Objects.equals(a.getName(), name))
.map(JpaRoleAttributeEntity::getValue)
.collect(Collectors.toList());
}
@Override
public void setAttributes(Map<String, List<String>> attributes) {
checkEntityVersionForUpdate();
for (Iterator<JpaRoleAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
JpaRoleAttributeEntity attr = iterator.next();
iterator.remove();
attr.setRole(null);
}
if (attributes != null) {
for (Map.Entry<String, List<String>> entry : attributes.entrySet()) {
setAttribute(entry.getKey(), entry.getValue());
}
}
}
@Override
public void setAttribute(String name, List<String> 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<JpaRoleAttributeEntity> 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());
}
}

View file

@ -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;
}
}

View file

@ -48,9 +48,11 @@ limitations under the License.
<column name="realmid"/> <column name="realmid"/>
<column name="clientid"/> <column name="clientid"/>
</createIndex> </createIndex>
<!--
<ext:createJsonIndex tableName="client" indexName="client_scopeMappings"> <ext:createJsonIndex tableName="client" indexName="client_scopeMappings">
<ext:column jsonColumn="metadata" jsonProperty="fScopeMappings"/> <ext:column jsonColumn="metadata" jsonProperty="fScopeMappings"/>
</ext:createJsonIndex> </ext:createJsonIndex>
-->
<createTable tableName="client_attribute"> <createTable tableName="client_attribute">
<column name="id" type="UUID"> <column name="id" type="UUID">

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<!-- format of id of changelog file names: jpa-roles-changelog-${org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_ROLE}.xml -->
<include file="META-INF/roles/jpa-roles-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="keycloak-jpa-default"> <persistence-unit name="keycloak-jpa-default">
<!--clients-->
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity</class> <class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity</class>
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity</class> <class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity</class>
<!--roles-->
<class>org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity</class>
<class>org.keycloak.models.map.storage.jpa.role.entity.JpaRoleAttributeEntity</class>
</persistence-unit> </persistence-unit>
</persistence> </persistence>

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<!-- format of id of changeSet: roles-${org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_ROLE} -->
<changeSet author="keycloak" id="roles-1">
<createTable tableName="role">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="metadata" type="json"/>
</createTable>
<ext:addGeneratedColumn tableName="role">
<ext:column name="entityversion" type="INTEGER" jsonColumn="metadata" jsonProperty="entityVersion"/>
<ext:column name="realmid" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fRealmId"/>
<ext:column name="name" type="VARCHAR(36)" jsonColumn="metadata" jsonProperty="fName"/>
<ext:column name="clientid" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fClientId"/>
<ext:column name="description" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fDescription"/>
</ext:addGeneratedColumn>
<createIndex tableName="role" indexName="role_entityVersion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="role" indexName="role_realmId_clientid_name" unique="true">
<column name="realmid"/>
<column name="clientid"/>
<column name="name"/>
</createIndex>
<!--
<ext:createJsonIndex tableName="role" indexName="role_gin">
<ext:column jsonColumn="metadata" jsonProperty="fName"/>
<ext:column jsonColumn="metadata" jsonProperty="fDescription"/>
</ext:createJsonIndex>
-->
<createTable tableName="role_attribute">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fk_role" type="UUID">
<constraints foreignKeyName="role_attr_fk_role_fkey" references="role(id)" deleteCascade="true"/>
</column>
<column name="name" type="VARCHAR(255)"/>
<column name="value" type="text"/>
</createTable>
<createIndex tableName="role_attribute" indexName="role_attr_fk_role">
<column name="fk_role"/>
</createIndex>
<createIndex tableName="role_attribute" indexName="role_attr_name_value">
<column name="name"/>
<column name="VALUE(255)" valueComputed="VALUE(255)"/>
</createIndex>
<modifySql dbms="postgresql">
<replace replace="VALUE(255)" with="(value::varchar(250))"/>
</modifySql>
</changeSet>
</databaseChangeLog>

View file

@ -16,8 +16,6 @@
*/ */
package org.keycloak.models.map.role; package org.keycloak.models.map.role;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.keycloak.models.map.annotations.GenerateEntityImplementations; import org.keycloak.models.map.annotations.GenerateEntityImplementations;
import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.AbstractEntity;
@ -47,10 +45,10 @@ public interface MapRoleEntity extends AbstractEntity, UpdatableEntity, EntityWi
this.updated |= id != null; this.updated |= id != null;
} }
@Override
public Boolean isClientRole() {
return getClientId() != null;
} }
default Boolean isComposite() {
return ! (getCompositeRoles() == null || getCompositeRoles().isEmpty());
} }
Boolean isClientRole(); Boolean isClientRole();

View file

@ -61,7 +61,7 @@ public class MapRoleProvider implements RoleProvider {
@Override @Override
public RoleModel addRealmRole(RealmModel realm, String id, String name) { public RoleModel addRealmRole(RealmModel realm, String id, String name) {
if (getRealmRole(realm, name) != null) { 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()); LOG.tracef("addRealmRole(%s, %s, %s)%s", realm, id, name, getShortStackTrace());
@ -118,7 +118,7 @@ public class MapRoleProvider implements RoleProvider {
@Override @Override
public RoleModel addClientRole(ClientModel client, String id, String name) { public RoleModel addClientRole(ClientModel client, String id, String name) {
if (getClientRole(client, name) != null) { 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()); LOG.tracef("addClientRole(%s, %s, %s)%s", client, id, name, getShortStackTrace());

View file

@ -122,7 +122,6 @@ public class MapFieldPredicates {
put(ROLE_PREDICATES, RoleModel.SearchableFields.DESCRIPTION, MapRoleEntity::getDescription); put(ROLE_PREDICATES, RoleModel.SearchableFields.DESCRIPTION, MapRoleEntity::getDescription);
put(ROLE_PREDICATES, RoleModel.SearchableFields.NAME, MapRoleEntity::getName); put(ROLE_PREDICATES, RoleModel.SearchableFields.NAME, MapRoleEntity::getName);
put(ROLE_PREDICATES, RoleModel.SearchableFields.IS_CLIENT_ROLE, MapRoleEntity::isClientRole); 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.REALM_ID, MapUserEntity::getRealmId);
put(USER_PREDICATES, UserModel.SearchableFields.USERNAME, MapUserEntity::getUsername); put(USER_PREDICATES, UserModel.SearchableFields.USERNAME, MapUserEntity::getUsername);

View file

@ -38,7 +38,6 @@ public interface RoleModel {
public static final SearchableModelField<RoleModel> NAME = new SearchableModelField<>("name", String.class); public static final SearchableModelField<RoleModel> NAME = new SearchableModelField<>("name", String.class);
public static final SearchableModelField<RoleModel> DESCRIPTION = new SearchableModelField<>("description", String.class); public static final SearchableModelField<RoleModel> DESCRIPTION = new SearchableModelField<>("description", String.class);
public static final SearchableModelField<RoleModel> IS_CLIENT_ROLE = new SearchableModelField<>("isClientRole", Boolean.class); public static final SearchableModelField<RoleModel> IS_CLIENT_ROLE = new SearchableModelField<>("isClientRole", Boolean.class);
public static final SearchableModelField<RoleModel> IS_COMPOSITE_ROLE = new SearchableModelField<>("isCompositeRole", Boolean.class);
} }
String getName(); String getName();