diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index cab098e318..592c1e2d16 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -21,20 +21,17 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.ModelException; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.jpa.entities.ClientAttributeEntity; import org.keycloak.models.jpa.entities.ClientEntity; import org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity; -import org.keycloak.models.jpa.entities.ClientScopeEntity; -import org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity; import org.keycloak.models.jpa.entities.ProtocolMapperEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; @@ -44,6 +41,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -302,25 +300,45 @@ public class ClientAdapter implements ClientModel, JpaModel { @Override public void setAttribute(String name, String value) { - entity.getAttributes().put(name, value); + for (ClientAttributeEntity attr : entity.getAttributes()) { + if (attr.getName().equals(name)) { + attr.setValue(value); + return; + } + } + ClientAttributeEntity attr = new ClientAttributeEntity(); + attr.setName(name); + attr.setValue(value); + attr.setClient(entity); + em.persist(attr); + entity.getAttributes().add(attr); } @Override public void removeAttribute(String name) { - entity.getAttributes().remove(name); + Iterator it = entity.getAttributes().iterator(); + while (it.hasNext()) { + ClientAttributeEntity attr = it.next(); + if (attr.getName().equals(name)) { + it.remove(); + em.remove(attr); + } + } } @Override public String getAttribute(String name) { - return entity.getAttributes().get(name); + return getAttributes().get(name); } @Override public Map getAttributes() { - Map copy = new HashMap<>(); - copy.putAll(entity.getAttributes()); - return copy; + Map attrs = new HashMap<>(); + for (ClientAttributeEntity attr : entity.getAttributes()) { + attrs.put(attr.getName(), attr.getValue()); + } + return attrs; } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java index 0521ba9310..13d84e80fa 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java @@ -24,6 +24,7 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.jpa.entities.ClientScopeAttributeEntity; import org.keycloak.models.jpa.entities.ClientScopeEntity; import org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity; import org.keycloak.models.jpa.entities.ProtocolMapperEntity; @@ -34,6 +35,7 @@ import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -282,18 +284,37 @@ public class ClientScopeAdapter implements ClientScopeModel, JpaModel it = entity.getAttributes().iterator(); + while (it.hasNext()) { + ClientScopeAttributeEntity attr = it.next(); + if (attr.getName().equals(name)) { + it.remove(); + em.remove(attr); + } + } } @Override public String getAttribute(String name) { - return entity.getAttributes().get(name); + return getAttributes().get(name); } public static ClientScopeEntity toClientScopeEntity(ClientScopeModel model, EntityManager em) { @@ -305,9 +326,11 @@ public class ClientScopeAdapter implements ClientScopeModel, JpaModel getAttributes() { - Map copy = new HashMap<>(); - copy.putAll(entity.getAttributes()); - return copy; + Map attrs = new HashMap<>(); + for (ClientScopeAttributeEntity attr : entity.getAttributes()) { + attrs.put(attr.getName(), attr.getValue()); + } + return attrs; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientAttributeEntity.java new file mode 100644 index 0000000000..1292dc726f --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientAttributeEntity.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017 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.jpa.entities; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + + +/** + * @author Marek Posolda + */ +@Table(name="CLIENT_ATTRIBUTES") +@Entity +@IdClass(ClientAttributeEntity.Key.class) +public class ClientAttributeEntity { + + @Id + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name = "CLIENT_ID") + protected ClientEntity client; + + @Id + @Column(name="NAME") + protected String name; + + @Column(name = "VALUE", length = 4000) + protected String value; + + public ClientEntity getClient() { + return client; + } + + public void setClient(ClientEntity client) { + this.client = client; + } + + 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; + } + + + public static class Key implements Serializable { + + protected ClientEntity client; + + protected String name; + + public Key() { + } + + public Key(ClientEntity client, String name) { + this.client = client; + this.name = name; + } + + public ClientEntity getClient() { + return client; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClientAttributeEntity.Key key = (ClientAttributeEntity.Key) o; + + if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; + if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = client != null ? client.getId().hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof ClientAttributeEntity)) return false; + + ClientAttributeEntity key = (ClientAttributeEntity) o; + + if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; + if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = client != null ? client.getId().hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index cc0672fb3b..874bc8251d 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -104,11 +104,8 @@ public class ClientEntity { @CollectionTable(name = "REDIRECT_URIS", joinColumns={ @JoinColumn(name="CLIENT_ID") }) protected Set redirectUris = new HashSet(); - @ElementCollection - @MapKeyColumn(name="NAME") - @Column(name="VALUE", length = 4000) - @CollectionTable(name="CLIENT_ATTRIBUTES", joinColumns={ @JoinColumn(name="CLIENT_ID") }) - protected Map attributes = new HashMap(); + @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client") + protected Collection attributes = new ArrayList<>(); @ElementCollection @MapKeyColumn(name="BINDING_NAME") @@ -278,11 +275,11 @@ public class ClientEntity { this.fullScopeAllowed = fullScopeAllowed; } - public Map getAttributes() { + public Collection getAttributes() { return attributes; } - public void setAttributes(Map attributes) { + public void setAttributes(Collection attributes) { this.attributes = attributes; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeAttributeEntity.java new file mode 100644 index 0000000000..6be9991de9 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeAttributeEntity.java @@ -0,0 +1,140 @@ +/* + * Copyright 2017 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.jpa.entities; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +/** + * @author Marek Posolda + */ +@Table(name="CLIENT_SCOPE_ATTRIBUTES") +@Entity +@IdClass(ClientScopeAttributeEntity.Key.class) +public class ClientScopeAttributeEntity { + + @Id + @ManyToOne(fetch= FetchType.LAZY) + @JoinColumn(name = "SCOPE_ID") + protected ClientScopeEntity clientScope; + + @Id + @Column(name="NAME") + protected String name; + + @Column(name = "VALUE", length = 2048) + protected String value; + + public ClientScopeEntity getClientScope() { + return clientScope; + } + + public void setClientScope(ClientScopeEntity 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; + } + + + public static class Key implements Serializable { + + protected ClientScopeEntity clientScope; + + protected String name; + + public Key() { + } + + public Key(ClientScopeEntity clientScope, String name) { + this.clientScope = clientScope; + this.name = name; + } + + public ClientScopeEntity getClientScope() { + return clientScope; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClientScopeAttributeEntity.Key key = (ClientScopeAttributeEntity.Key) o; + + if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false; + if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = clientScope != null ? clientScope.getId().hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof ClientScopeAttributeEntity)) return false; + + ClientScopeAttributeEntity key = (ClientScopeAttributeEntity) o; + + if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false; + if (name != null ? !name.equals(key.name != null ? key.name : null) : key.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = clientScope != null ? clientScope.getId().hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java index 7fe193e7b8..eddabe5e5b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java @@ -22,22 +22,17 @@ import org.hibernate.annotations.Nationalized; import javax.persistence.Access; import javax.persistence.AccessType; import javax.persistence.CascadeType; -import javax.persistence.CollectionTable; import javax.persistence.Column; -import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; -import javax.persistence.MapKeyColumn; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; /** * @author Bill Burke @@ -66,11 +61,8 @@ public class ClientScopeEntity { private String protocol; - @ElementCollection - @MapKeyColumn(name="NAME") - @Column(name="VALUE", length = 2048) - @CollectionTable(name="CLIENT_SCOPE_ATTRIBUTES", joinColumns={ @JoinColumn(name="SCOPE_ID") }) - protected Map attributes = new HashMap(); + @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "clientScope") + protected Collection attributes = new ArrayList<>(); public RealmEntity getRealm() { return realm; @@ -120,11 +112,11 @@ public class ClientScopeEntity { this.protocol = protocol; } - public Map getAttributes() { + public Collection getAttributes() { return attributes; } - public void setAttributes(Map attributes) { + public void setAttributes(Collection attributes) { this.attributes = attributes; } diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml index d888203d4c..1649fa1363 100755 --- a/model/jpa/src/main/resources/META-INF/persistence.xml +++ b/model/jpa/src/main/resources/META-INF/persistence.xml @@ -21,6 +21,7 @@ version="1.0"> org.keycloak.models.jpa.entities.ClientEntity + org.keycloak.models.jpa.entities.ClientAttributeEntity org.keycloak.models.jpa.entities.CredentialEntity org.keycloak.models.jpa.entities.CredentialAttributeEntity org.keycloak.models.jpa.entities.RealmEntity @@ -54,6 +55,7 @@ org.keycloak.models.jpa.entities.GroupRoleMappingEntity org.keycloak.models.jpa.entities.UserGroupMembershipEntity org.keycloak.models.jpa.entities.ClientScopeEntity + org.keycloak.models.jpa.entities.ClientScopeAttributeEntity org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity org.keycloak.models.jpa.entities.DefaultClientScopeRealmMappingEntity diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java index 5ffe8b65ab..937028dbc6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.keycloak.admin.client.resource.ClientScopesResource; import org.keycloak.admin.client.resource.ProtocolMappersResource; import org.keycloak.admin.client.resource.RoleMappingResource; +import org.keycloak.common.util.ObjectUtil; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.AccountRoles; @@ -135,12 +136,19 @@ public class ClientScopeTest extends AbstractClientTest { scopeRep.setName("scope1"); scopeRep.setDescription("scope1-desc"); scopeRep.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + + Map attrs = new HashMap<>(); + attrs.put("someAttr", "someAttrValue"); + attrs.put("emptyAttr", ""); + scopeRep.setAttributes(attrs); String scope1Id = createClientScope(scopeRep); // Assert created attributes scopeRep = clientScopes().get(scope1Id).toRepresentation(); Assert.assertEquals("scope1", scopeRep.getName()); Assert.assertEquals("scope1-desc", scopeRep.getDescription()); + Assert.assertEquals("someAttrValue", scopeRep.getAttributes().get("someAttr")); + Assert.assertTrue(ObjectUtil.isBlank(scopeRep.getAttributes().get("emptyAttr"))); Assert.assertEquals(OIDCLoginProtocol.LOGIN_PROTOCOL, scopeRep.getProtocol()); @@ -149,6 +157,9 @@ public class ClientScopeTest extends AbstractClientTest { scopeRep.setDescription("scope1-desc-updated"); scopeRep.setProtocol(SamlProtocol.LOGIN_PROTOCOL); + // Test update attribute to some non-blank value + scopeRep.getAttributes().put("emptyAttr", "someValue"); + clientScopes().get(scope1Id).update(scopeRep); assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientScopeResourcePath(scope1Id), scopeRep, ResourceType.CLIENT_SCOPE); @@ -158,6 +169,8 @@ public class ClientScopeTest extends AbstractClientTest { Assert.assertEquals("scope1-updated", scopeRep.getName()); Assert.assertEquals("scope1-desc-updated", scopeRep.getDescription()); Assert.assertEquals(SamlProtocol.LOGIN_PROTOCOL, scopeRep.getProtocol()); + Assert.assertEquals("someAttrValue", scopeRep.getAttributes().get("someAttr")); + Assert.assertEquals("someValue", scopeRep.getAttributes().get("emptyAttr")); // Remove scope1 clientScopes().get(scope1Id).remove();