JPA map storage: Groups no-downtime store

Closes #9660
This commit is contained in:
vramik 2022-02-04 12:02:04 +01:00 committed by Hynek Mlnařík
parent 5d3fbbb158
commit 589606b1c1
25 changed files with 800 additions and 44 deletions

View file

@ -19,5 +19,6 @@ package org.keycloak.models.map.storage.jpa;
public interface Constants {
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_GROUP = 1;
public static final Integer CURRENT_SCHEMA_VERSION_ROLE = 1;
}

View file

@ -48,6 +48,7 @@ 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.GroupModel;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -62,6 +63,8 @@ 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.group.JpaGroupMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity;
import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaEntityVersionListener;
import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaOptimisticLockingListener;
import org.keycloak.models.map.storage.jpa.role.JpaRoleMapKeycloakTransaction;
@ -88,6 +91,8 @@ public class JpaMapStorageProviderFactory implements
.constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
//client-scope
.constructor(JpaClientScopeEntity.class, JpaClientScopeEntity::new)
//group
.constructor(JpaGroupEntity.class, JpaGroupEntity::new)
//role
.constructor(JpaRoleEntity.class, JpaRoleEntity::new)
.build();
@ -96,6 +101,7 @@ public class JpaMapStorageProviderFactory implements
static {
MODEL_TO_TX.put(ClientScopeModel.class, JpaClientScopeMapKeycloakTransaction::new);
MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new);
MODEL_TO_TX.put(GroupModel.class, JpaGroupMapKeycloakTransaction::new);
MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new);
}

View file

@ -66,7 +66,9 @@ public class JpaClientDelegateProvider extends JpaDelegateProvider<JpaClientEnti
default:
setDelegate(em.find(JpaClientEntity.class, UUID.fromString(getDelegate().getId())));
}
} else throw new IllegalStateException("Not a valid client field: " + field);
} else {
throw new IllegalStateException("Not a valid client field: " + field);
}
} else {
setDelegate(em.find(JpaClientEntity.class, UUID.fromString(getDelegate().getId())));
}

View file

@ -19,7 +19,6 @@ package org.keycloak.models.map.storage.jpa.client.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;
@ -536,12 +535,7 @@ public class JpaClientEntity extends AbstractClientEntity implements JpaRootEnti
@Override
public void removeAttribute(String name) {
for (Iterator<JpaClientAttributeEntity> iterator = attributes.iterator(); iterator.hasNext();) {
JpaClientAttributeEntity attr = iterator.next();
if (Objects.equals(attr.getName(), name)) {
iterator.remove();
}
}
attributes.removeIf(attr -> Objects.equals(attr.getName(), name));
}
@Override
@ -574,9 +568,7 @@ public class JpaClientEntity extends AbstractClientEntity implements JpaRootEnti
@Override
public void setAttributes(Map<String, List<String>> attributes) {
for (Iterator<JpaClientAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
iterator.remove();
}
this.attributes.clear();
if (attributes != null) {
for (Map.Entry<String, List<String>> attrEntry : attributes.entrySet()) {
setAttribute(attrEntry.getKey(), attrEntry.getValue());

View file

@ -64,7 +64,9 @@ public class JpaClientScopeDelegateProvider extends JpaDelegateProvider<JpaClien
default:
setDelegate(em.find(JpaClientScopeEntity.class, UUID.fromString(getDelegate().getId())));
}
} else throw new IllegalStateException("Not a valid client scope field: " + field);
} else {
throw new IllegalStateException("Not a valid client scope field: " + field);
}
} else {
setDelegate(em.find(JpaClientScopeEntity.class, UUID.fromString(getDelegate().getId())));
}

View file

@ -19,7 +19,6 @@ 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;
@ -213,12 +212,7 @@ public class JpaClientScopeEntity extends AbstractClientScopeEntity implements J
@Override
public void removeAttribute(String name) {
for (Iterator<JpaClientScopeAttributeEntity> iterator = attributes.iterator(); iterator.hasNext();) {
JpaClientScopeAttributeEntity attr = iterator.next();
if (Objects.equals(attr.getName(), name)) {
iterator.remove();
}
}
attributes.removeIf(attr -> Objects.equals(attr.getName(), name));
}
@Override
@ -251,9 +245,7 @@ public class JpaClientScopeEntity extends AbstractClientScopeEntity implements J
@Override
public void setAttributes(Map<String, List<String>> attributes) {
for (Iterator<JpaClientScopeAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
iterator.remove();
}
this.attributes.clear();
if (attributes != null) {
for (Map.Entry<String, List<String>> attrEntry : attributes.entrySet()) {
setAttribute(attrEntry.getKey(), attrEntry.getValue());

View file

@ -0,0 +1,66 @@
/*
* 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.group;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import org.keycloak.models.GroupModel;
import org.keycloak.models.map.group.MapGroupEntity;
import org.keycloak.models.map.group.MapGroupEntityDelegate;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_GROUP;
import org.keycloak.models.map.storage.jpa.group.delegate.JpaGroupDelegateProvider;
import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity;
import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
public class JpaGroupMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaGroupEntity, MapGroupEntity, GroupModel> {
@SuppressWarnings("unchecked")
public JpaGroupMapKeycloakTransaction(EntityManager em) {
super(JpaGroupEntity.class, em);
}
@Override
public Selection<JpaGroupEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaGroupEntity> root) {
return cb.construct(JpaGroupEntity.class,
root.get("id"),
root.get("version"),
root.get("entityVersion"),
root.get("realmId"),
root.get("name"),
root.get("parentId")
);
}
@Override
public void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(CURRENT_SCHEMA_VERSION_GROUP);
}
@Override
public JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaGroupModelCriteriaBuilder();
}
@Override
protected MapGroupEntity mapToEntityDelegate(JpaGroupEntity original) {
return new MapGroupEntityDelegate(new JpaGroupDelegateProvider(original, em));
}
}

View file

@ -0,0 +1,151 @@
/*
* 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.group;
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.Predicate;
import javax.persistence.criteria.Root;
import org.keycloak.models.GroupModel;
import org.keycloak.models.map.common.StringKeyConvertor;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.storage.SearchableModelField;
public class JpaGroupModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaGroupEntity, GroupModel, JpaGroupModelCriteriaBuilder> {
public JpaGroupModelCriteriaBuilder() {
super(JpaGroupModelCriteriaBuilder::new);
}
private JpaGroupModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaGroupEntity>, Predicate> predicateFunc) {
super(JpaGroupModelCriteriaBuilder::new, predicateFunc);
}
@Override
@SuppressWarnings("unchecked")
public JpaGroupModelCriteriaBuilder compare(SearchableModelField<? super GroupModel> modelField, Operator op, Object... value) {
switch (op) {
case EQ:
if (modelField.equals(GroupModel.SearchableFields.REALM_ID) ||
modelField.equals(GroupModel.SearchableFields.NAME)) {
validateValue(value, modelField, op, String.class);
return new JpaGroupModelCriteriaBuilder((cb, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else if (modelField.equals(GroupModel.SearchableFields.PARENT_ID)) {
if (value.length == 1 && Objects.isNull(value[0])) {
return new JpaGroupModelCriteriaBuilder((cb, root) ->
cb.isNull(root.get("parentId"))
);
}
validateValue(value, modelField, op, String.class);
return new JpaGroupModelCriteriaBuilder((cb, root) ->
cb.equal(root.get("parentId"), value[0])
);
} else if (modelField.equals(GroupModel.SearchableFields.ASSIGNED_ROLE)) {
validateValue(value, modelField, op, String.class);
return new JpaGroupModelCriteriaBuilder((cb, root) ->
cb.isTrue(cb.function("@>",
Boolean.TYPE,
cb.function("->", JsonbType.class, root.get("metadata"), cb.literal("fGrantedRoles")),
cb.literal(convertToJson(value[0]))))
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case IN:
if (modelField.equals(GroupModel.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));
}
if (collectionValues.isEmpty()) {
return new JpaGroupModelCriteriaBuilder((cb, root) -> cb.or());
}
return new JpaGroupModelCriteriaBuilder((cb, root) -> {
CriteriaBuilder.In<UUID> in = cb.in(root.get("id"));
for (Object id : collectionValues) {
try {
in.value(StringKeyConvertor.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(GroupModel.SearchableFields.NAME)) {
validateValue(value, modelField, op, String.class);
return new JpaGroupModelCriteriaBuilder((cb, root) ->
cb.like(cb.lower(root.get(modelField.getName())), value[0].toString().toLowerCase())
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case NOT_EXISTS:
if (modelField.equals(GroupModel.SearchableFields.PARENT_ID)) {
return new JpaGroupModelCriteriaBuilder((cb, root) ->
cb.isNull(root.get("parentId"))
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
default:
throw new CriterionNotSupportedException(modelField, op);
}
}
}

View file

@ -0,0 +1,76 @@
/*
* 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.group.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.group.MapGroupEntity;
import org.keycloak.models.map.group.MapGroupEntityFields;
import org.keycloak.models.map.storage.jpa.JpaDelegateProvider;
import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity;
public class JpaGroupDelegateProvider extends JpaDelegateProvider<JpaGroupEntity> implements DelegateProvider<MapGroupEntity> {
private final EntityManager em;
public JpaGroupDelegateProvider(JpaGroupEntity delegate, EntityManager em) {
super(delegate);
this.em = em;
}
@Override
public MapGroupEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapGroupEntity>> field, Object... parameters) {
if (getDelegate().isMetadataInitialized()) return getDelegate();
if (isRead) {
if (field instanceof MapGroupEntityFields) {
switch ((MapGroupEntityFields) field) {
case ID:
case REALM_ID:
case NAME:
case PARENT_ID:
return getDelegate();
case ATTRIBUTES:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaGroupEntity> query = cb.createQuery(JpaGroupEntity.class);
Root<JpaGroupEntity> root = query.from(JpaGroupEntity.class);
root.fetch("attributes", JoinType.LEFT);
query.select(root).where(cb.equal(root.get("id"), UUID.fromString(getDelegate().getId())));
setDelegate(em.createQuery(query).getSingleResult());
break;
default:
setDelegate(em.find(JpaGroupEntity.class, UUID.fromString(getDelegate().getId())));
}
} else {
throw new IllegalStateException("Not a valid group field: " + field);
}
} else {
setDelegate(em.find(JpaGroupEntity.class, UUID.fromString(getDelegate().getId())));
}
return getDelegate();
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.group.entity;
import javax.persistence.Entity;
import javax.persistence.Table;
import org.keycloak.models.map.storage.jpa.JpaAttributeEntity;
@Entity
@Table(name = "kc_group_attribute")
public class JpaGroupAttributeEntity extends JpaAttributeEntity<JpaGroupEntity> {
public JpaGroupAttributeEntity() {
}
public JpaGroupAttributeEntity(JpaGroupEntity root, String name, String value) {
super(root, name, value);
}
}

View file

@ -0,0 +1,257 @@
/*
* 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.group.entity;
import java.util.HashMap;
import java.util.HashSet;
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.group.MapGroupEntity.AbstractGroupEntity;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_GROUP;
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 = "kc_group", uniqueConstraints = {@UniqueConstraint(columnNames = {"realmId", "name", "parentId"})})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaGroupEntity extends AbstractGroupEntity 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 JpaGroupMetadata 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;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String parentId;
@OneToMany(mappedBy = "root", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<JpaGroupAttributeEntity> attributes = new HashSet<>();
/**
* No-argument constructor, used by hibernate to instantiate entities.
*/
public JpaGroupEntity() {
this.metadata = new JpaGroupMetadata();
}
public JpaGroupEntity(DeepCloner cloner) {
this.metadata = new JpaGroupMetadata(cloner);
}
/**
* Used by hibernate when calling cb.construct from read(QueryParameters) method.
* It is used to select group without metadata(json) field.
*/
public JpaGroupEntity(UUID id, int version, Integer entityVersion, String realmId,
String name, String parentId) {
this.id = id;
this.version = version;
this.entityVersion = entityVersion;
this.realmId = realmId;
this.name = name;
this.parentId = parentId;
this.metadata = null;
}
public boolean isMetadataInitialized() {
return metadata != null;
}
@Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return metadata.getEntityVersion();
return entityVersion;
}
@Override
public Integer getCurrentSchemaVersion() {
return CURRENT_SCHEMA_VERSION_GROUP;
}
@Override
public void setEntityVersion(Integer entityVersion) {
metadata.setEntityVersion(entityVersion);
}
@Override
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) {
metadata.setRealmId(realmId);
}
@Override
public String getName() {
if (isMetadataInitialized()) return metadata.getName();
return name;
}
@Override
public void setName(String name) {
metadata.setName(name);
}
@Override
public void setParentId(String parentId) {
metadata.setParentId(parentId);
}
@Override
public String getParentId() {
if (isMetadataInitialized()) return metadata.getParentId();
return parentId;
}
@Override
public Set<String> getGrantedRoles() {
return metadata.getGrantedRoles();
}
@Override
public void setGrantedRoles(Set<String> grantedRoles) {
metadata.setGrantedRoles(grantedRoles);
}
@Override
public void addGrantedRole(String role) {
metadata.addGrantedRole(role);
}
@Override
public void removeGrantedRole(String role) {
metadata.removeGrantedRole(role);
}
@Override
public Map<String, List<String>> getAttributes() {
Map<String, List<String>> result = new HashMap<>();
for (JpaGroupAttributeEntity 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) {
this.attributes.clear();
if (attributes != null) {
for (Map.Entry<String, List<String>> entry : attributes.entrySet()) {
setAttribute(entry.getKey(), entry.getValue());
}
}
}
@Override
public List<String> getAttribute(String name) {
return attributes.stream()
.filter(a -> Objects.equals(a.getName(), name))
.map(JpaGroupAttributeEntity::getValue)
.collect(Collectors.toList());
}
@Override
public void setAttribute(String name, List<String> values) {
removeAttribute(name);
for (String value : values) {
JpaGroupAttributeEntity attribute = new JpaGroupAttributeEntity(this, name, value);
attributes.add(attribute);
}
}
@Override
public void removeAttribute(String name) {
attributes.removeIf(attr -> Objects.equals(attr.getName(), name));
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaGroupEntity)) return false;
return Objects.equals(getId(), ((JpaGroupEntity) 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.group.entity;
import java.io.Serializable;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.group.MapGroupEntityImpl;
public class JpaGroupMetadata extends MapGroupEntityImpl implements Serializable {
public JpaGroupMetadata(DeepCloner cloner) {
super(cloner);
}
public JpaGroupMetadata() {
super();
}
private Integer entityVersion;
public Integer getEntityVersion() {
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
this.entityVersion = entityVersion;
}
}

View file

@ -24,11 +24,14 @@ 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_GROUP;
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.group.entity.JpaGroupMetadata;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaClientMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaClientScopeMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaGroupMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaRoleMigration;
import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleMetadata;
@ -40,6 +43,7 @@ public class JpaEntityMigration {
static {
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(JpaGroupMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_GROUP, tree, JpaGroupMigration.MIGRATORS));
MIGRATIONS.put(JpaRoleMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_ROLE, tree, JpaRoleMigration.MIGRATORS));
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
public class JpaGroupMigration {
public static final List<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
}

View file

@ -98,6 +98,10 @@ public class JpaRoleModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaRole
collectionValues = new HashSet(Arrays.asList(value));
}
if (collectionValues.isEmpty()) {
return new JpaRoleModelCriteriaBuilder((cb, root) -> cb.or());
}
return new JpaRoleModelCriteriaBuilder((cb, root) -> {
In<UUID> in = cb.in(root.get("id"));
for (Object id : collectionValues) {

View file

@ -64,7 +64,9 @@ public class JpaRoleDelegateProvider extends JpaDelegateProvider<JpaRoleEntity>
default:
setDelegate(em.find(JpaRoleEntity.class, UUID.fromString(getDelegate().getId())));
}
} else throw new IllegalStateException("Not a valid role field: " + field);
} else {
throw new IllegalStateException("Not a valid role field: " + field);
}
} else {
setDelegate(em.find(JpaRoleEntity.class, UUID.fromString(getDelegate().getId())));
}

View file

@ -18,7 +18,6 @@ package org.keycloak.models.map.storage.jpa.role.entity;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -242,9 +241,7 @@ public class JpaRoleEntity extends AbstractRoleEntity implements JpaRootEntity {
@Override
public void setAttributes(Map<String, List<String>> attributes) {
for (Iterator<JpaRoleAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
iterator.remove();
}
this.attributes.clear();
if (attributes != null) {
for (Map.Entry<String, List<String>> entry : attributes.entrySet()) {
setAttribute(entry.getKey(), entry.getValue());
@ -263,12 +260,7 @@ public class JpaRoleEntity extends AbstractRoleEntity implements JpaRootEntity {
@Override
public void removeAttribute(String name) {
for (Iterator<JpaRoleAttributeEntity> iterator = attributes.iterator(); iterator.hasNext();) {
JpaRoleAttributeEntity attr = iterator.next();
if (Objects.equals(attr.getName(), name)) {
iterator.remove();
}
}
attributes.removeIf(attr -> Objects.equals(attr.getName(), name));
}
@Override

View file

@ -0,0 +1,74 @@
<?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: groups-${org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_GROUP} -->
<changeSet author="keycloak" id="groups-1">
<createTable tableName="kc_group">
<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="kc_group">
<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:column name="parentid" type="VARCHAR(36)" jsonColumn="metadata" jsonProperty="fParentId"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_group" indexName="group_entityVersion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="kc_group" indexName="group_realmId_name_parentId" unique="true">
<column name="realmid"/>
<column name="name"/>
<column name="parentid"/>
</createIndex>
<createTable tableName="kc_group_attribute">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fk_root" type="UUID">
<constraints foreignKeyName="group_attr_fk_root_fkey" references="kc_group(id)" deleteCascade="true"/>
</column>
<column name="name" type="VARCHAR(255)"/>
<column name="value" type="text"/>
</createTable>
<createIndex tableName="kc_group_attribute" indexName="group_attr_fk_root">
<column name="fk_root"/>
</createIndex>
<createIndex tableName="kc_group_attribute" indexName="group_attr_name_value">
<column name="name"/>
<column name="VALUE(255)" valueComputed="VALUE(255)"/>
</createIndex>
<modifySql dbms="postgresql,cockroachdb">
<replace replace="VALUE(255)" with="(value::varchar(250))"/>
</modifySql>
</changeSet>
</databaseChangeLog>

View file

@ -18,6 +18,6 @@ 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 -->
<!-- format of id of changelog file names: jpa-client-scopes-changelog-${org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT_SCOPE}.xml -->
<include file="META-INF/client-scopes/jpa-client-scopes-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -18,6 +18,6 @@ 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-clients-changelog-${org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT}.xml -->
<!-- format of id of changelog file names: jpa-clients-changelog-${org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT}.xml -->
<include file="META-INF/clients/jpa-clients-changelog-1.xml"/>
</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-groups-changelog-${org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_GROUP.xml -->
<include file="META-INF/groups/jpa-groups-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -18,6 +18,6 @@ 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 -->
<!-- format of id of changelog file names: jpa-roles-changelog-${org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ROLE}.xml -->
<include file="META-INF/roles/jpa-roles-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -7,6 +7,9 @@
<!--clients-->
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity</class>
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity</class>
<!--groups-->
<class>org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity</class>
<class>org.keycloak.models.map.storage.jpa.group.entity.JpaGroupAttributeEntity</class>
<!--roles-->
<class>org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity</class>
<class>org.keycloak.models.map.storage.jpa.role.entity.JpaRoleAttributeEntity</class>

View file

@ -36,8 +36,8 @@ limitations under the License.
</createTable>
<ext:addGeneratedColumn tableName="kc_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="realmid" type="VARCHAR(36)" jsonColumn="metadata" jsonProperty="fRealmId"/>
<ext:column name="name" type="VARCHAR(255)" 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>

View file

@ -33,7 +33,6 @@ import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.UnaryOperator;
@ -122,7 +121,7 @@ public class MapGroupProvider implements GroupProvider {
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
if (Objects.equals(onlyTopGroups, Boolean.TRUE)) {
mcb = mcb.compare(SearchableFields.PARENT_ID, Operator.EQ, (Object) null);
mcb = mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS);
}
return tx.getCount(withCriteria(mcb));
@ -185,12 +184,14 @@ public class MapGroupProvider implements GroupProvider {
public GroupModel createGroup(RealmModel realm, String id, String name, GroupModel toParent) {
LOG.tracef("createGroup(%s, %s, %s, %s)%s", realm, id, name, toParent, getShortStackTrace());
// Check Db constraint: uniqueConstraints = { @UniqueConstraint(columnNames = {"REALM_ID", "PARENT_GROUP", "NAME"})}
String parentId = toParent == null ? null : toParent.getId();
DefaultModelCriteria<GroupModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId())
.compare(SearchableFields.PARENT_ID, Operator.EQ, parentId)
.compare(SearchableFields.NAME, Operator.EQ, name);
mcb = toParent == null ?
mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS) :
mcb.compare(SearchableFields.PARENT_ID, Operator.EQ, toParent.getId());
if (tx.getCount(withCriteria(mcb)) > 0) {
throw new ModelDuplicateException("Group with name '" + name + "' in realm " + realm.getName() + " already exists for requested parent" );
}
@ -254,12 +255,14 @@ public class MapGroupProvider implements GroupProvider {
return;
}
String parentId = toParent == null ? null : toParent.getId();
DefaultModelCriteria<GroupModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId())
.compare(SearchableFields.PARENT_ID, Operator.EQ, parentId)
.compare(SearchableFields.NAME, Operator.EQ, group.getName());
mcb = toParent == null ?
mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS) :
mcb.compare(SearchableFields.PARENT_ID, Operator.EQ, toParent.getId());
try (Stream<MapGroupEntity> possibleSiblings = tx.read(withCriteria(mcb))) {
if (possibleSiblings.findAny().isPresent()) {
throw new ModelDuplicateException("Parent already contains subgroup named '" + group.getName() + "'");