KEYCLOAK-18565 JPA roles no-downtime store
This commit is contained in:
parent
213b1f5042
commit
7b89d151c1
23 changed files with 1090 additions and 71 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<Class<?>, Function<EntityManager, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>();
|
||||
static {
|
||||
MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new);
|
||||
MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new);
|
||||
}
|
||||
|
||||
public MapKeycloakTransaction createTransaction(Class<?> modelType, EntityManager em) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<ClientModel, JpaClientModelCriteriaBuilder> {
|
||||
|
||||
private BiFunction<CriteriaBuilder, Root<JpaClientEntity>, Predicate> predicateFunc = null;
|
||||
public class JpaClientModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaClientEntity, ClientModel, JpaClientModelCriteriaBuilder> {
|
||||
|
||||
public JpaClientModelCriteriaBuilder() {
|
||||
super(JpaClientModelCriteriaBuilder::new);
|
||||
}
|
||||
|
||||
private JpaClientModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaClientEntity>, 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<Clien
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,8 +35,8 @@ public class JpaClientDelegateProvider implements DelegateProvider<MapClientEnti
|
|||
private JpaClientEntity delegate;
|
||||
private final EntityManager em;
|
||||
|
||||
public JpaClientDelegateProvider(JpaClientEntity deledate, EntityManager em) {
|
||||
this.delegate = deledate;
|
||||
public JpaClientDelegateProvider(JpaClientEntity delegate, EntityManager em) {
|
||||
this.delegate = delegate;
|
||||
this.em = em;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,11 @@ import org.keycloak.models.map.common.DeepCloner;
|
|||
import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT;
|
||||
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 = "client")
|
||||
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
|
||||
|
|
|
@ -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.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_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 {
|
||||
|
||||
static final Map<Class<?>, BiFunction<ObjectNode, Integer, ObjectNode>> 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<Function<ObjectNode, ObjectNode>> migrators) {
|
||||
|
|
|
@ -76,6 +76,9 @@ public class JsonbType extends AbstractSingleColumnStandardBasicType<Object> imp
|
|||
abstract class IgnoredMetadataFieldsMixIn {
|
||||
@JsonIgnore public abstract String getId();
|
||||
@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() {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -48,9 +48,11 @@ limitations under the License.
|
|||
<column name="realmid"/>
|
||||
<column name="clientid"/>
|
||||
</createIndex>
|
||||
<!--
|
||||
<ext:createJsonIndex tableName="client" indexName="client_scopeMappings">
|
||||
<ext:column jsonColumn="metadata" jsonProperty="fScopeMappings"/>
|
||||
</ext:createJsonIndex>
|
||||
-->
|
||||
|
||||
<createTable tableName="client_attribute">
|
||||
<column name="id" type="UUID">
|
||||
|
|
|
@ -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>
|
|
@ -1,7 +1,11 @@
|
|||
<?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-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.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>
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isClientRole() {
|
||||
return getClientId() != null;
|
||||
}
|
||||
|
||||
default Boolean isComposite() {
|
||||
return ! (getCompositeRoles() == null || getCompositeRoles().isEmpty());
|
||||
}
|
||||
|
||||
Boolean isClientRole();
|
||||
|
|
|
@ -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());
|
||||
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -38,7 +38,6 @@ public interface RoleModel {
|
|||
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> IS_CLIENT_ROLE = new SearchableModelField<>("isClientRole", Boolean.class);
|
||||
public static final SearchableModelField<RoleModel> IS_COMPOSITE_ROLE = new SearchableModelField<>("isCompositeRole", Boolean.class);
|
||||
}
|
||||
|
||||
String getName();
|
||||
|
|
Loading…
Reference in a new issue