diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java index 8718ec98e9..197cd4f36f 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java @@ -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; } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java index 900752b546..34ffbf1898 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java @@ -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); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java index 5f7c6c9a04..e94a77e031 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java @@ -66,7 +66,9 @@ public class JpaClientDelegateProvider extends JpaDelegateProvider 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> attributes) { - for (Iterator iterator = this.attributes.iterator(); iterator.hasNext();) { - iterator.remove(); - } + this.attributes.clear(); if (attributes != null) { for (Map.Entry> attrEntry : attributes.entrySet()) { setAttribute(attrEntry.getKey(), attrEntry.getValue()); diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientscope/delegate/JpaClientScopeDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientscope/delegate/JpaClientScopeDelegateProvider.java index e4883460df..fce6e2f849 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientscope/delegate/JpaClientScopeDelegateProvider.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientscope/delegate/JpaClientScopeDelegateProvider.java @@ -64,7 +64,9 @@ public class JpaClientScopeDelegateProvider extends JpaDelegateProvider 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> attributes) { - for (Iterator iterator = this.attributes.iterator(); iterator.hasNext();) { - iterator.remove(); - } + this.attributes.clear(); if (attributes != null) { for (Map.Entry> attrEntry : attributes.entrySet()) { setAttribute(attrEntry.getKey(), attrEntry.getValue()); diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapKeycloakTransaction.java new file mode 100644 index 0000000000..f933e15205 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapKeycloakTransaction.java @@ -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 { + + @SuppressWarnings("unchecked") + public JpaGroupMapKeycloakTransaction(EntityManager em) { + super(JpaGroupEntity.class, em); + } + + @Override + public Selection selectCbConstruct(CriteriaBuilder cb, Root 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)); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupModelCriteriaBuilder.java new file mode 100644 index 0000000000..8d8f585699 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupModelCriteriaBuilder.java @@ -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 { + + public JpaGroupModelCriteriaBuilder() { + super(JpaGroupModelCriteriaBuilder::new); + } + + private JpaGroupModelCriteriaBuilder(BiFunction, Predicate> predicateFunc) { + super(JpaGroupModelCriteriaBuilder::new, predicateFunc); + } + + @Override + @SuppressWarnings("unchecked") + public JpaGroupModelCriteriaBuilder compare(SearchableModelField 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 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); + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/delegate/JpaGroupDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/delegate/JpaGroupDelegateProvider.java new file mode 100644 index 0000000000..b513eff3d6 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/delegate/JpaGroupDelegateProvider.java @@ -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 implements DelegateProvider { + + private final EntityManager em; + + public JpaGroupDelegateProvider(JpaGroupEntity delegate, EntityManager em) { + super(delegate); + this.em = em; + } + + @Override + public MapGroupEntity getDelegate(boolean isRead, Enum> 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 query = cb.createQuery(JpaGroupEntity.class); + Root 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(); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupAttributeEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupAttributeEntity.java new file mode 100644 index 0000000000..51b9e0629e --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupAttributeEntity.java @@ -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 { + + public JpaGroupAttributeEntity() { + } + + public JpaGroupAttributeEntity(JpaGroupEntity root, String name, String value) { + super(root, name, value); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupEntity.java new file mode 100644 index 0000000000..ec5ef157cc --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupEntity.java @@ -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 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 getGrantedRoles() { + return metadata.getGrantedRoles(); + } + + @Override + public void setGrantedRoles(Set 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> getAttributes() { + Map> result = new HashMap<>(); + for (JpaGroupAttributeEntity attribute : attributes) { + List values = result.getOrDefault(attribute.getName(), new LinkedList<>()); + values.add(attribute.getValue()); + result.put(attribute.getName(), values); + } + return result; + } + + @Override + public void setAttributes(Map> attributes) { + this.attributes.clear(); + if (attributes != null) { + for (Map.Entry> entry : attributes.entrySet()) { + setAttribute(entry.getKey(), entry.getValue()); + } + } + } + + @Override + public List 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 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()); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupMetadata.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupMetadata.java new file mode 100644 index 0000000000..311f49cdb6 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/entity/JpaGroupMetadata.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.storage.jpa.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; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java index 14d49ea32a..bd46edb2ae 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java @@ -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)); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaGroupMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaGroupMigration.java new file mode 100644 index 0000000000..cb4fa7aba7 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaGroupMigration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +public class JpaGroupMigration { + + public static final List> MIGRATORS = Arrays.asList( + o -> o // no migration yet + ); + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java index e79d7761ed..c185e29e15 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleModelCriteriaBuilder.java @@ -98,6 +98,10 @@ public class JpaRoleModelCriteriaBuilder extends JpaModelCriteriaBuilder cb.or()); + } + return new JpaRoleModelCriteriaBuilder((cb, root) -> { In in = cb.in(root.get("id")); for (Object id : collectionValues) { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java index 0ab9b6cfa3..52edaff4d2 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaRoleDelegateProvider.java @@ -64,7 +64,9 @@ public class JpaRoleDelegateProvider extends JpaDelegateProvider 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()))); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java index 92263186dd..b824712110 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/entity/JpaRoleEntity.java @@ -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> attributes) { - for (Iterator iterator = this.attributes.iterator(); iterator.hasNext();) { - iterator.remove(); - } + this.attributes.clear(); if (attributes != null) { for (Map.Entry> 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 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 diff --git a/model/map-jpa/src/main/resources/META-INF/groups/jpa-groups-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/groups/jpa-groups-changelog-1.xml new file mode 100644 index 0000000000..28cf03b780 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/groups/jpa-groups-changelog-1.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-client-scopes-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-client-scopes-changelog.xml index e6d503c990..31a3b180b3 100644 --- a/model/map-jpa/src/main/resources/META-INF/jpa-client-scopes-changelog.xml +++ b/model/map-jpa/src/main/resources/META-INF/jpa-client-scopes-changelog.xml @@ -18,6 +18,6 @@ limitations under the License. - + diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog.xml index 35fc6a11e7..ffb1e888bc 100644 --- a/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog.xml +++ b/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog.xml @@ -18,6 +18,6 @@ limitations under the License. - + diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-groups-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-groups-changelog.xml new file mode 100644 index 0000000000..54b98e29ca --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/jpa-groups-changelog.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml index 5c6efafaa6..90b5ee805c 100644 --- a/model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml +++ b/model/map-jpa/src/main/resources/META-INF/jpa-roles-changelog.xml @@ -18,6 +18,6 @@ limitations under the License. - + diff --git a/model/map-jpa/src/main/resources/META-INF/persistence.xml b/model/map-jpa/src/main/resources/META-INF/persistence.xml index 43186c97d2..e055e5f261 100644 --- a/model/map-jpa/src/main/resources/META-INF/persistence.xml +++ b/model/map-jpa/src/main/resources/META-INF/persistence.xml @@ -7,6 +7,9 @@ org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity + + org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity + org.keycloak.models.map.storage.jpa.group.entity.JpaGroupAttributeEntity org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity org.keycloak.models.map.storage.jpa.role.entity.JpaRoleAttributeEntity diff --git a/model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml index c53af98108..a7bf9d783e 100644 --- a/model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml +++ b/model/map-jpa/src/main/resources/META-INF/roles/jpa-roles-changelog-1.xml @@ -36,8 +36,8 @@ limitations under the License. - - + + diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java index 6fc4725d17..ed6077df75 100644 --- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java @@ -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 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 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 possibleSiblings = tx.read(withCriteria(mcb))) { if (possibleSiblings.findAny().isPresent()) { throw new ModelDuplicateException("Parent already contains subgroup named '" + group.getName() + "'");