JPA map storage: Client scope no-downtime store

Closes #9663
This commit is contained in:
vramik 2022-01-20 18:45:53 +01:00 committed by Hynek Mlnařík
parent fe0cb36284
commit 13e02d5f09
19 changed files with 1032 additions and 262 deletions

View file

@ -17,6 +17,7 @@
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;
public static final Integer CURRENT_SCHEMA_VERSION_CLIENT = 1;
public static final Integer CURRENT_SCHEMA_VERSION_CLIENT_SCOPE = 1;
public static final Integer CURRENT_SCHEMA_VERSION_ROLE = 1;
}

View file

@ -0,0 +1,168 @@
/*
* 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 java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.function.BiFunction;
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.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import org.keycloak.connections.jpa.JpaKeycloakTransaction;
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.StringKeyConvertor;
import org.keycloak.models.map.common.StringKeyConvertor.UUIDKey;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.QueryParameters;
import static org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory.CLONER;
import static org.keycloak.utils.StreamsUtil.closing;
public abstract class JpaMapKeycloakTransaction<RE extends JpaRootEntity, E extends AbstractEntity, M> extends JpaKeycloakTransaction implements MapKeycloakTransaction<E, M> {
private final Class<RE> entityType;
@SuppressWarnings("unchecked")
public JpaMapKeycloakTransaction(Class<RE> entityType, EntityManager em) {
super(em);
this.entityType = entityType;
}
protected abstract Selection<? extends RE> selectCbConstruct(CriteriaBuilder cb, Root<RE> root);
protected abstract void setEntityVersion(JpaRootEntity entity);
protected abstract JpaModelCriteriaBuilder createJpaModelCriteriaBuilder();
protected abstract E mapToEntityDelegate(RE original);
@Override
@SuppressWarnings("unchecked")
public E create(E mapEntity) {
JpaRootEntity jpaEntity = entityType.cast(CLONER.from(mapEntity));
CLONER.from(mapEntity);
if (mapEntity.getId() == null) {
jpaEntity.setId(StringKeyConvertor.UUIDKey.INSTANCE.yieldNewUniqueKey().toString());
}
setEntityVersion(jpaEntity);
em.persist(jpaEntity);
return (E) jpaEntity;
}
@Override
@SuppressWarnings("unchecked")
public E read(String key) {
if (key == null) return null;
UUID uuid = StringKeyConvertor.UUIDKey.INSTANCE.fromStringSafe(key);
if (uuid == null) return null;
return (E) em.find(entityType, uuid);
}
@Override
@SuppressWarnings("unchecked")
public Stream<E> read(QueryParameters<M> queryParameters) {
JpaModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createJpaModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<RE> query = cb.createQuery(entityType);
Root<RE> root = query.from(entityType);
query.select(selectCbConstruct(cb, root));
//ordering
if (!queryParameters.getOrderBy().isEmpty()) {
List<Order> orderByList = new LinkedList<>();
for (QueryParameters.OrderBy<M> 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);
}
BiFunction<CriteriaBuilder, Root<RE>, Predicate> predicateFunc = mcb.getPredicateFunc();
if (predicateFunc != null) query.where(predicateFunc.apply(cb, root));
return closing(paginateQuery(em.createQuery(query), queryParameters.getOffset(), queryParameters.getLimit()).getResultStream())
.map(this::mapToEntityDelegate);
}
@Override
@SuppressWarnings("unchecked")
public long getCount(QueryParameters<M> queryParameters) {
JpaModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createJpaModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
Root<RE> root = countQuery.from(entityType);
countQuery.select(cb.count(root));
BiFunction<CriteriaBuilder, Root<RE>, Predicate> predicateFunc = mcb.getPredicateFunc();
if (predicateFunc != null) countQuery.where(predicateFunc.apply(cb, root));
return em.createQuery(countQuery).getSingleResult();
}
@Override
@SuppressWarnings("unchecked")
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(entityType, uuid));
return true;
}
@Override
@SuppressWarnings("unchecked")
public long delete(QueryParameters<M> queryParameters) {
JpaModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createJpaModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaDelete<RE> deleteQuery = cb.createCriteriaDelete(entityType);
Root<RE> root = deleteQuery.from(entityType);
BiFunction<CriteriaBuilder, Root<RE>, Predicate> predicateFunc = mcb.getPredicateFunc();
if (predicateFunc != null) deleteQuery.where(predicateFunc.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

@ -39,6 +39,7 @@ import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -51,6 +52,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.clientscope.JpaClientScopeMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeEntity;
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;
@ -73,14 +76,17 @@ public class JpaMapStorageProviderFactory implements
//client
.constructor(JpaClientEntity.class, JpaClientEntity::new)
.constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
//client-scope
.constructor(JpaClientScopeEntity.class, JpaClientScopeEntity::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);
MODEL_TO_TX.put(ClientScopeModel.class, JpaClientScopeMapKeycloakTransaction::new);
MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new);
MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new);
}
public MapKeycloakTransaction createTransaction(Class<?> modelType, EntityManager em) {

View file

@ -1,13 +1,13 @@
/*
* 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.
@ -16,13 +16,27 @@
*/
package org.keycloak.models.map.storage.jpa;
import java.io.Serializable;
import org.keycloak.models.map.common.AbstractEntity;
/**
* Interface for all root entities in the JPA storage.
*/
public interface JpaRootEntity {
public interface JpaRootEntity extends AbstractEntity, Serializable {
/**
* Version of the JPA entity used for optimistic locking
*/
int getVersion();
/**
* @return current supported version of the JPA entity used for schema versioning.
*/
Integer getEntityVersion();
/**
* @param entityVersion sets current supported version to JPA entity.
*/
void setEntityVersion(Integer entityVersion);
}

View file

@ -16,147 +16,52 @@
*/
package org.keycloak.models.map.storage.jpa.client;
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 javax.persistence.criteria.Selection;
import org.keycloak.models.ClientModel;
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityDelegate;
import org.keycloak.models.map.common.StringKeyConvertor.UUIDKey;
import org.keycloak.models.map.storage.jpa.client.delegate.JpaClientDelegateProvider;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import static org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory.CLONER;
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_CLIENT;
import static org.keycloak.utils.StreamsUtil.closing;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT;
import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.client.delegate.JpaClientDelegateProvider;
public class JpaClientMapKeycloakTransaction extends JpaKeycloakTransaction implements MapKeycloakTransaction<MapClientEntity, ClientModel> {
public class JpaClientMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaClientEntity, MapClientEntity, ClientModel> {
@SuppressWarnings("unchecked")
public JpaClientMapKeycloakTransaction(EntityManager em) {
super(em);
super(JpaClientEntity.class, em);
}
@Override
public MapClientEntity create(MapClientEntity mapEntity) {
JpaClientEntity jpaEntity = (JpaClientEntity) CLONER.from(mapEntity);
if (mapEntity.getId() == null) {
jpaEntity.setId(UUIDKey.INSTANCE.yieldNewUniqueKey().toString());
}
jpaEntity.setEntityVersion(SUPPORTED_VERSION_CLIENT);
em.persist(jpaEntity);
return jpaEntity;
public Selection<JpaClientEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaClientEntity> root) {
return cb.construct(JpaClientEntity.class,
root.get("id"),
root.get("version"),
root.get("entityVersion"),
root.get("realmId"),
root.get("clientId"),
root.get("protocol"),
root.get("enabled")
);
}
@Override
public MapClientEntity read(String key) {
if (key == null) return null;
UUID uuid = UUIDKey.INSTANCE.fromStringSafe(key);
if (uuid == null) return null;
return em.find(JpaClientEntity.class, uuid);
public void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(CURRENT_SCHEMA_VERSION_CLIENT);
}
@Override
public Stream<MapClientEntity> read(QueryParameters<ClientModel> queryParameters) {
JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaClientEntity> query = cb.createQuery(JpaClientEntity.class);
Root<JpaClientEntity> root = query.from(JpaClientEntity.class);
query.select(cb.construct(JpaClientEntity.class,
root.get("id"),
root.get("version"),
root.get("entityVersion"),
root.get("realmId"),
root.get("clientId"),
root.get("protocol"),
root.get("enabled")
));
//ordering
if (!queryParameters.getOrderBy().isEmpty()) {
List<Order> orderByList = new LinkedList<>();
for (QueryParameters.OrderBy<ClientModel> 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(c -> new MapClientEntityDelegate(new JpaClientDelegateProvider(c, em)));
public JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaClientModelCriteriaBuilder();
}
@Override
public long getCount(QueryParameters<ClientModel> queryParameters) {
JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
Root<JpaClientEntity> root = countQuery.from(JpaClientEntity.class);
countQuery.select(cb.count(root));
if (mcb.getPredicateFunc() != null) countQuery.where(mcb.getPredicateFunc().apply(cb, root));
return em.createQuery(countQuery).getSingleResult();
protected MapClientEntity mapToEntityDelegate(JpaClientEntity original) {
return new MapClientEntityDelegate(new JpaClientDelegateProvider(original, em));
}
@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(JpaClientEntity.class, uuid));
return true;
}
@Override
public long delete(QueryParameters<ClientModel> queryParameters) {
JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaDelete<JpaClientEntity> deleteQuery = cb.createCriteriaDelete(JpaClientEntity.class);
Root<JpaClientEntity> root = deleteQuery.from(JpaClientEntity.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

@ -16,7 +16,6 @@
*/
package org.keycloak.models.map.storage.jpa.client.entity;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@ -44,8 +43,7 @@ import org.hibernate.annotations.TypeDefs;
import org.keycloak.models.map.client.MapClientEntity.AbstractClientEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntity;
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.CURRENT_SCHEMA_VERSION_CLIENT;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
@ -62,7 +60,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
)
})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaClientEntity extends AbstractClientEntity implements Serializable, JpaRootEntity {
public class JpaClientEntity extends AbstractClientEntity implements JpaRootEntity {
@Id
@Column
@ -137,16 +135,18 @@ public class JpaClientEntity extends AbstractClientEntity implements Serializabl
*/
private void checkEntityVersionForUpdate() {
Integer ev = getEntityVersion();
if (ev != null && ev < SUPPORTED_VERSION_CLIENT) {
setEntityVersion(SUPPORTED_VERSION_CLIENT);
if (ev != null && ev < CURRENT_SCHEMA_VERSION_CLIENT) {
setEntityVersion(CURRENT_SCHEMA_VERSION_CLIENT);
}
}
@Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return metadata.getEntityVersion();
return entityVersion;
}
@Override
public void setEntityVersion(Integer entityVersion) {
metadata.setEntityVersion(entityVersion);
}

View file

@ -0,0 +1,64 @@
/*
* 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.clientscope;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.map.clientscope.MapClientScopeEntity;
import org.keycloak.models.map.clientscope.MapClientScopeEntityDelegate;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT_SCOPE;
import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.clientscope.delegate.JpaClientScopeDelegateProvider;
import org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeEntity;
public class JpaClientScopeMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaClientScopeEntity, MapClientScopeEntity, ClientScopeModel> {
@SuppressWarnings("unchecked")
public JpaClientScopeMapKeycloakTransaction(EntityManager em) {
super(JpaClientScopeEntity.class, em);
}
@Override
protected Selection<JpaClientScopeEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaClientScopeEntity> root) {
return cb.construct(JpaClientScopeEntity.class,
root.get("id"),
root.get("version"),
root.get("entityVersion"),
root.get("realmId"),
root.get("name"));
}
@Override
public void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(CURRENT_SCHEMA_VERSION_CLIENT_SCOPE);
}
@Override
public JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaClientScopeModelCriteriaBuilder();
}
@Override
protected MapClientScopeEntity mapToEntityDelegate(JpaClientScopeEntity original) {
return new MapClientScopeEntityDelegate(new JpaClientScopeDelegateProvider(original, em));
}
}

View file

@ -0,0 +1,60 @@
/*
* 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.clientscope;
import java.util.function.BiFunction;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientScopeModel.SearchableFields;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeEntity;
import org.keycloak.storage.SearchableModelField;
public class JpaClientScopeModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaClientScopeEntity, ClientScopeModel, JpaClientScopeModelCriteriaBuilder> {
public JpaClientScopeModelCriteriaBuilder() {
super(JpaClientScopeModelCriteriaBuilder::new);
}
private JpaClientScopeModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaClientScopeEntity>, Predicate> predicateFunc) {
super(JpaClientScopeModelCriteriaBuilder::new, predicateFunc);
}
@Override
public JpaClientScopeModelCriteriaBuilder compare(SearchableModelField<? super ClientScopeModel> modelField, Operator op, Object... value) {
switch (op) {
case EQ:
if (modelField.equals(SearchableFields.REALM_ID) ||
modelField.equals(SearchableFields.NAME)) {
validateValue(value, modelField, op, String.class);
return new JpaClientScopeModelCriteriaBuilder((cb, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} 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.clientscope.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.clientscope.MapClientScopeEntity;
import org.keycloak.models.map.clientscope.MapClientScopeEntityFields;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeEntity;
public class JpaClientScopeDelegateProvider implements DelegateProvider<MapClientScopeEntity> {
private JpaClientScopeEntity delegate;
private final EntityManager em;
public JpaClientScopeDelegateProvider(JpaClientScopeEntity delegate, EntityManager em) {
this.delegate = delegate;
this.em = em;
}
@Override
public MapClientScopeEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapClientScopeEntity>> field, Object... parameters) {
if (delegate.isMetadataInitialized()) return delegate;
if (isRead) {
if (field instanceof MapClientScopeEntityFields) {
switch ((MapClientScopeEntityFields) field) {
case ID:
case REALM_ID:
case NAME:
return delegate;
case ATTRIBUTES:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaClientScopeEntity> query = cb.createQuery(JpaClientScopeEntity.class);
Root<JpaClientScopeEntity> root = query.from(JpaClientScopeEntity.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(JpaClientScopeEntity.class, UUID.fromString(delegate.getId()));
}
} else throw new IllegalStateException("Not a valid client scope field: " + field);
} else {
delegate = em.find(JpaClientScopeEntity.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.clientscope.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 = "client_scope_attribute")
public class JpaClientScopeAttributeEntity implements Serializable {
@Id
@Column
@GeneratedValue
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="fk_client_scope")
private JpaClientScopeEntity clientScope;
@Column
private String name;
@Nationalized
@Column
private String value;
public JpaClientScopeAttributeEntity() {
}
public JpaClientScopeAttributeEntity(JpaClientScopeEntity clientScope, String name, String value) {
this.clientScope = clientScope;
this.name = name;
this.value = value;
}
public UUID getId() {
return id;
}
public JpaClientScopeEntity getClientScope() {
return clientScope;
}
public void setClientScope(JpaClientScopeEntity clientScope) {
this.clientScope = clientScope;
}
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 JpaClientScopeAttributeEntity)) return false;
JpaClientScopeAttributeEntity that = (JpaClientScopeAttributeEntity) obj;
return Objects.equals(getClientScope(), that.getClientScope()) &&
Objects.equals(getName(), that.getName()) &&
Objects.equals(getValue(), that.getValue());
}
}

View file

@ -0,0 +1,293 @@
/*
* 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.clientscope.entity;
import java.util.Collection;
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.client.MapProtocolMapperEntity;
import org.keycloak.models.map.clientscope.MapClientScopeEntity.AbstractClientScopeEntity;
import org.keycloak.models.map.common.DeepCloner;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT_SCOPE;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
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_scope", uniqueConstraints = {@UniqueConstraint(columnNames = {"realmId", "name"})})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaClientScopeEntity extends AbstractClientScopeEntity implements JpaRootEntity {
@Id
@Column
private UUID id;
//used for implicit optimistic locking
@Version
@Column
private int version;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private final JpaClientScopeMetadata 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 name;
@OneToMany(mappedBy = "clientScope", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<JpaClientScopeAttributeEntity> attributes = new HashSet<>();
/**
* No-argument constructor, used by hibernate to instantiate entities.
*/
public JpaClientScopeEntity() {
this.metadata = new JpaClientScopeMetadata();
}
public JpaClientScopeEntity(DeepCloner cloner) {
this.metadata = new JpaClientScopeMetadata(cloner);
}
/**
* Used by hibernate when calling cb.construct from read(QueryParameters) method.
* It is used to select client without metadata(json) field.
*/
public JpaClientScopeEntity(UUID id, int version, Integer entityVersion, String realmId, String name) {
this.id = id;
this.version = version;
this.entityVersion = entityVersion;
this.realmId = realmId;
this.name = name;
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 < CURRENT_SCHEMA_VERSION_CLIENT_SCOPE) {
setEntityVersion(CURRENT_SCHEMA_VERSION_CLIENT_SCOPE);
}
}
@Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return metadata.getEntityVersion();
return entityVersion;
}
@Override
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 void setRealmId(String realmId) {
checkEntityVersionForUpdate();
metadata.setRealmId(realmId);
}
@Override
public Set<MapProtocolMapperEntity> getProtocolMappers() {
return metadata.getProtocolMappers();
}
@Override
public void addProtocolMapper(MapProtocolMapperEntity mapping) {
checkEntityVersionForUpdate();
metadata.addProtocolMapper(mapping);
}
@Override
public void addScopeMapping(String id) {
checkEntityVersionForUpdate();
metadata.addScopeMapping(id);
}
@Override
public void removeScopeMapping(String id) {
checkEntityVersionForUpdate();
metadata.removeScopeMapping(id);
}
@Override
public Collection<String> getScopeMappings() {
return metadata.getScopeMappings();
}
@Override
public String getDescription() {
return metadata.getDescription();
}
@Override
public void setDescription(String description) {
checkEntityVersionForUpdate();
metadata.setDescription(description);
}
@Override
public String getName() {
if (isMetadataInitialized()) return metadata.getName();
return name;
}
@Override
public void setName(String name) {
checkEntityVersionForUpdate();
metadata.setName(name);
}
@Override
public String getProtocol() {
return metadata.getProtocol();
}
@Override
public void setProtocol(String protocol) {
checkEntityVersionForUpdate();
metadata.setProtocol(protocol);
}
@Override
public void removeAttribute(String name) {
checkEntityVersionForUpdate();
for (Iterator<JpaClientScopeAttributeEntity> iterator = attributes.iterator(); iterator.hasNext();) {
JpaClientScopeAttributeEntity attr = iterator.next();
if (Objects.equals(attr.getName(), name)) {
iterator.remove();
attr.setClientScope(null);
}
}
}
@Override
public void setAttribute(String name, List<String> values) {
checkEntityVersionForUpdate();
removeAttribute(name);
for (String value : values) {
JpaClientScopeAttributeEntity attribute = new JpaClientScopeAttributeEntity(this, name, value);
attributes.add(attribute);
}
}
@Override
public List<String> getAttribute(String name) {
return attributes.stream()
.filter(a -> Objects.equals(a.getName(), name))
.map(JpaClientScopeAttributeEntity::getValue)
.collect(Collectors.toList());
}
@Override
public Map<String, List<String>> getAttributes() {
Map<String, List<String>> result = new HashMap<>();
for (JpaClientScopeAttributeEntity attribute : attributes) {
List<String> values = result.getOrDefault(attribute.getName(), new LinkedList<>());
values.add(attribute.getValue());
result.put(attribute.getName(), values);
}
return result;
}
@Override
public void setAttributes(Map<String, List<String>> attributes) {
checkEntityVersionForUpdate();
for (Iterator<JpaClientScopeAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
JpaClientScopeAttributeEntity attr = iterator.next();
iterator.remove();
attr.setClientScope(null);
}
if (attributes != null) {
for (Map.Entry<String, List<String>> attrEntry : attributes.entrySet()) {
setAttribute(attrEntry.getKey(), attrEntry.getValue());
}
}
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaClientScopeEntity)) return false;
return Objects.equals(getId(), ((JpaClientScopeEntity) 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.clientscope.entity;
import java.io.Serializable;
import org.keycloak.models.map.clientscope.MapClientScopeEntityImpl;
import org.keycloak.models.map.common.DeepCloner;
public class JpaClientScopeMetadata extends MapClientScopeEntityImpl implements Serializable {
public JpaClientScopeMetadata(DeepCloner cloner) {
super(cloner);
}
public JpaClientScopeMetadata() {
super();
}
private Integer entityVersion;
public Integer getEntityVersion() {
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
this.entityVersion = entityVersion;
}
}

View file

@ -22,19 +22,25 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT_SCOPE;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ROLE;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientMetadata;
import org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeMetadata;
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.JpaClientScopeMigration;
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));
MIGRATIONS.put(JpaClientMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_CLIENT, tree, JpaClientMigration.MIGRATORS));
MIGRATIONS.put(JpaClientScopeMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_CLIENT_SCOPE, tree, JpaClientScopeMigration.MIGRATORS));
MIGRATIONS.put(JpaRoleMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_ROLE, tree, JpaRoleMigration.MIGRATORS));
}
private static ObjectNode migrateTreeTo(int entityVersion, Integer supportedVersion, ObjectNode node, List<Function<ObjectNode, ObjectNode>> migrators) {

View file

@ -0,0 +1,29 @@
/*
* 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 JpaClientScopeMigration {
public static final List<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
}

View file

@ -16,146 +16,52 @@
*/
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 javax.persistence.criteria.Selection;
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 static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ROLE;
import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
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 class JpaRoleMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaRoleEntity, MapRoleEntity, RoleModel> {
@SuppressWarnings("unchecked")
public JpaRoleMapKeycloakTransaction(EntityManager em) {
super(em);
super(JpaRoleEntity.class, 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;
public Selection<JpaRoleEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaRoleEntity> root) {
return cb.construct(JpaRoleEntity.class,
root.get("id"),
root.get("version"),
root.get("entityVersion"),
root.get("realmId"),
root.get("clientId"),
root.get("name"),
root.get("description")
);
}
@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);
public void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(CURRENT_SCHEMA_VERSION_ROLE);
}
@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("version"),
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)));
public JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaRoleModelCriteriaBuilder();
}
@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();
protected MapRoleEntity mapToEntityDelegate(JpaRoleEntity original) {
return new MapRoleEntityDelegate(new JpaRoleDelegateProvider(original, em));
}
@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

@ -16,7 +16,6 @@
*/
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;
@ -42,8 +41,7 @@ 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 static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ROLE;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
@ -55,7 +53,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
@Entity
@Table(name = "role", uniqueConstraints = {@UniqueConstraint(columnNames = {"realmId", "clientId", "name"})})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaRoleEntity extends AbstractRoleEntity implements Serializable, JpaRootEntity {
public class JpaRoleEntity extends AbstractRoleEntity implements JpaRootEntity {
@Id
@Column
@ -129,16 +127,18 @@ public class JpaRoleEntity extends AbstractRoleEntity implements Serializable, J
*/
private void checkEntityVersionForUpdate() {
Integer ev = getEntityVersion();
if (ev != null && ev < SUPPORTED_VERSION_ROLE) {
setEntityVersion(SUPPORTED_VERSION_ROLE);
if (ev != null && ev < CURRENT_SCHEMA_VERSION_ROLE) {
setEntityVersion(CURRENT_SCHEMA_VERSION_ROLE);
}
}
@Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return metadata.getEntityVersion();
return entityVersion;
}
@Override
public void setEntityVersion(Integer entityVersion) {
metadata.setEntityVersion(entityVersion);
}

View file

@ -0,0 +1,72 @@
<?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: client-scopes-${org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT_SCOPE} -->
<changeSet author="keycloak" id="client-scopes-1">
<createTable tableName="client_scope">
<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="client_scope">
<ext:column name="entityversion" type="INTEGER" jsonColumn="metadata" jsonProperty="entityVersion"/>
<ext:column name="realmid" type="VARCHAR(36)" jsonColumn="metadata" jsonProperty="fRealmId"/>
<ext:column name="name" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fName"/>
</ext:addGeneratedColumn>
<createIndex tableName="client_scope" indexName="client_scope_entityVersion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="client_scope" indexName="client_realmId_name" unique="true">
<column name="realmid"/>
<column name="name"/>
</createIndex>
<createTable tableName="client_scope_attribute">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fk_client_scope" type="UUID">
<constraints foreignKeyName="client_scope_attr_fk_client_scope_fkey" references="client_scope(id)" deleteCascade="true"/>
</column>
<column name="name" type="VARCHAR(255)"/>
<column name="value" type="text"/>
</createTable>
<createIndex tableName="client_scope_attribute" indexName="client_scope_attr_fk_client_scope">
<column name="fk_client_scope"/>
</createIndex>
<createIndex tableName="client_scope_attribute" indexName="client_scope_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

@ -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-client-scopes-changelog-${org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT_SCOPE}.xml -->
<include file="META-INF/client-scopes/jpa-client-scopes-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -1,6 +1,9 @@
<?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">
<!--client-scopes-->
<class>org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeEntity</class>
<class>org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeAttributeEntity</class>
<!--clients-->
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity</class>
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity</class>