keycloak-memoizeit
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java 11(+10 -1)
services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java 9(+4 -5)
services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java 4(+2 -2)
services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java 40(+40 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java 12(+6 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java 4(+4 -0)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
index 8f361b3..3467b5b 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
@@ -17,6 +17,8 @@
package org.keycloak.representations.idm;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -35,6 +37,7 @@ public class RoleRepresentation {
protected Composites composites;
private Boolean clientRole;
private String containerId;
+ protected Map<String, List<String>> attributes;
public static class Composites {
protected Set<String> realm;
@@ -138,4 +141,21 @@ public class RoleRepresentation {
public void setContainerId(String containerId) {
this.containerId = containerId;
}
+
+ public Map<String, List<String>> getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map<String, List<String>> attributes) {
+ this.attributes = attributes;
+ }
+
+ public RoleRepresentation singleAttribute(String name, String value) {
+ if (attributes == null) {
+ attributes = new HashMap<>();
+ }
+
+ attributes.put(name, Arrays.asList(value));
+ return this;
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java
index 5ee29bc..47bfa72 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRole.java
@@ -17,11 +17,15 @@
package org.keycloak.models.cache.infinispan.entities;
+import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
+import org.keycloak.models.cache.infinispan.LazyLoader;
import java.util.HashSet;
import java.util.Set;
+import java.util.function.Supplier;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -34,6 +38,7 @@ public class CachedRole extends AbstractRevisioned implements InRealm {
final protected String description;
final protected boolean composite;
final protected Set<String> composites = new HashSet<String>();
+ private final LazyLoader<RoleModel, MultivaluedHashMap<String, String>> attributes;
public CachedRole(Long revision, RoleModel model, RealmModel realm) {
super(revision, model.getId());
@@ -46,7 +51,7 @@ public class CachedRole extends AbstractRevisioned implements InRealm {
composites.add(child.getId());
}
}
-
+ attributes = new DefaultLazyLoader<>(roleModel -> new MultivaluedHashMap<>(roleModel.getAttributes()), MultivaluedHashMap::new);
}
public String getName() {
@@ -68,4 +73,8 @@ public class CachedRole extends AbstractRevisioned implements InRealm {
public Set<String> getComposites() {
return composites;
}
+
+ public MultivaluedHashMap<String, String> getAttributes(Supplier<RoleModel> roleModel) {
+ return attributes.get(roleModel);
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
index 6a6ac86..51620a1 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
@@ -25,8 +25,13 @@ import org.keycloak.models.cache.infinispan.entities.CachedRealmRole;
import org.keycloak.models.cache.infinispan.entities.CachedRole;
import org.keycloak.models.utils.KeycloakModelUtils;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.function.Supplier;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -39,22 +44,25 @@ public class RoleAdapter implements RoleModel {
protected RealmCacheSession cacheSession;
protected RealmModel realm;
protected Set<RoleModel> composites;
+ private final Supplier<RoleModel> modelSupplier;
public RoleAdapter(CachedRole cached, RealmCacheSession session, RealmModel realm) {
this.cached = cached;
this.cacheSession = session;
this.realm = realm;
+ this.modelSupplier = this::getRoleModel;
}
protected void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());
- updated = cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm);
+ updated = modelSupplier.get();
if (updated == null) throw new IllegalStateException("Not found in database");
}
}
protected boolean invalidated;
+
public void invalidate() {
invalidated = true;
}
@@ -68,8 +76,6 @@ public class RoleAdapter implements RoleModel {
}
-
-
@Override
public String getName() {
if (isUpdated()) return updated.getName();
@@ -144,7 +150,7 @@ public class RoleAdapter implements RoleModel {
@Override
public String getContainerId() {
if (isClientRole()) {
- CachedClientRole appRole = (CachedClientRole)cached;
+ CachedClientRole appRole = (CachedClientRole) cached;
return appRole.getClientId();
} else {
return realm.getId();
@@ -157,7 +163,7 @@ public class RoleAdapter implements RoleModel {
if (cached instanceof CachedRealmRole) {
return realm;
} else {
- CachedClientRole appRole = (CachedClientRole)cached;
+ CachedClientRole appRole = (CachedClientRole) cached;
return realm.getClientById(appRole.getClientId());
}
}
@@ -168,6 +174,59 @@ public class RoleAdapter implements RoleModel {
}
@Override
+ public void setSingleAttribute(String name, String value) {
+ getDelegateForUpdate();
+ updated.setSingleAttribute(name, value);
+ }
+
+ @Override
+ public void setAttribute(String name, Collection<String> values) {
+ getDelegateForUpdate();
+ updated.setAttribute(name, values);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ getDelegateForUpdate();
+ updated.removeAttribute(name);
+ }
+
+ @Override
+ public String getFirstAttribute(String name) {
+ if (updated != null) {
+ return updated.getFirstAttribute(name);
+ }
+
+ return cached.getAttributes(modelSupplier).getFirst(name);
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ if (updated != null) {
+ return updated.getAttribute(name);
+ }
+
+ List<String> result = cached.getAttributes(modelSupplier).get(name);
+ if (result == null) {
+ result = Collections.emptyList();
+ }
+ return result;
+ }
+
+ @Override
+ public Map<String, List<String>> getAttributes() {
+ if (updated != null) {
+ return updated.getAttributes();
+ }
+
+ return cached.getAttributes(modelSupplier);
+ }
+
+ private RoleModel getRoleModel() {
+ return cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm);
+ }
+
+ @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof RoleModel)) return false;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleAttributeEntity.java
new file mode 100644
index 0000000..59b88a4
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleAttributeEntity.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2018 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 org.hibernate.annotations.Nationalized;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:leon.graser@bosch-si.com">Leon Graser</a>
+ */
+@NamedQueries({
+ @NamedQuery(name = "deleteRoleAttributesByNameAndUser", query = "delete from RoleAttributeEntity attr where attr.role.id = :roleId and attr.name = :name"),
+})
+@Table(name = "ROLE_ATTRIBUTE")
+@Entity
+public class RoleAttributeEntity {
+
+ @Id
+ @Column(name = "ID", length = 36)
+ @Access(AccessType.PROPERTY)
+ protected String id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "ROLE_ID")
+ protected RoleEntity role;
+
+ @Column(name = "NAME")
+ protected String name;
+
+ @Nationalized
+ @Column(name = "VALUE")
+ protected String value;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ 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 RoleEntity getRole() {
+ return role;
+ }
+
+ public void setRole(RoleEntity role) {
+ this.role = role;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ boolean result = false;
+
+ if (o instanceof RoleAttributeEntity) {
+ RoleAttributeEntity otherRole = (RoleAttributeEntity) o;
+ result = id.equals(otherRole.id);
+ }
+
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
index 526d559..d1ac58c 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
@@ -17,10 +17,14 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.BatchSize;
+import org.hibernate.annotations.Fetch;
+import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
+import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -31,9 +35,13 @@ import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
/**
@@ -93,6 +101,11 @@ public class RoleEntity {
@JoinTable(name = "COMPOSITE_ROLE", joinColumns = @JoinColumn(name = "COMPOSITE"), inverseJoinColumns = @JoinColumn(name = "CHILD_ROLE"))
private Set<RoleEntity> compositeRoles = new HashSet<>();
+ @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="role")
+ @Fetch(FetchMode.SELECT)
+ @BatchSize(size = 20)
+ protected List<RoleAttributeEntity> attributes = new ArrayList<>();
+
public String getId() {
return id;
}
@@ -109,7 +122,13 @@ public class RoleEntity {
this.realmId = realmId;
}
+ public Collection<RoleAttributeEntity> getAttributes() {
+ return attributes;
+ }
+ public void setAttributes(List<RoleAttributeEntity> attributes) {
+ this.attributes = attributes;
+ }
public String getName() {
return name;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
index 27b81ba..eb10c37 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
@@ -21,11 +21,19 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
+import org.keycloak.models.jpa.entities.RoleAttributeEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
+import javax.persistence.Query;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -116,6 +124,77 @@ public class RoleAdapter implements RoleModel, JpaModel<RoleEntity> {
return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>());
}
+ private void persistAttributeValue(String name, String value) {
+ RoleAttributeEntity attr = new RoleAttributeEntity();
+ attr.setId(KeycloakModelUtils.generateId());
+ attr.setName(name);
+ attr.setValue(value);
+ attr.setRole(role);
+ em.persist(attr);
+ role.getAttributes().add(attr);
+ }
+
+ @Override
+ public void setSingleAttribute(String name, String value) {
+ setAttribute(name, Collections.singletonList(value));
+ }
+
+ @Override
+ public void setAttribute(String name, Collection<String> values) {
+ removeAttribute(name);
+
+ for (String value : values) {
+ persistAttributeValue(name, value);
+ }
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ Collection<RoleAttributeEntity> attributes = role.getAttributes();
+ if (attributes == null) {
+ return;
+ }
+
+ Query query = em.createNamedQuery("deleteRoleAttributesByNameAndUser");
+ query.setParameter("name", name);
+ query.setParameter("roleId", role.getId());
+ query.executeUpdate();
+
+ attributes.removeIf(attribute -> attribute.getName().equals(name));
+ }
+
+ @Override
+ public String getFirstAttribute(String name) {
+ for (RoleAttributeEntity attribute : role.getAttributes()) {
+ if (attribute.getName().equals(name)) {
+ return attribute.getValue();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ List<String> attributes = new ArrayList<>();
+ for (RoleAttributeEntity attribute : role.getAttributes()) {
+ if (attribute.getName().equals(name)) {
+ attributes.add(attribute.getValue());
+ }
+ }
+ return attributes;
+ }
+
+ @Override
+ public Map<String, List<String>> getAttributes() {
+ Map<String, List<String>> map = new HashMap<>();
+ for (RoleAttributeEntity attribute : role.getAttributes()) {
+ map.computeIfAbsent(attribute.getName(), name -> new ArrayList<>()).add(attribute.getValue());
+ }
+
+ return map;
+ }
+
@Override
public boolean isClientRole() {
return role.isClientRole();
@@ -154,7 +233,7 @@ public class RoleAdapter implements RoleModel, JpaModel<RoleEntity> {
public static RoleEntity toRoleEntity(RoleModel model, EntityManager em) {
if (model instanceof RoleAdapter) {
- return ((RoleAdapter)model).getEntity();
+ return ((RoleAdapter) model).getEntity();
}
return em.getReference(RoleEntity.class, model.getId());
}
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.6.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.6.0.xml
index c902a8e..64ac38f 100644
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.6.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.6.0.xml
@@ -23,11 +23,31 @@
<where>NAME LIKE 'group.resource.%'</where>
</update>
</changeSet>
-
+
+ <changeSet author="keycloak" id="4.6.0-KEYCLOAK-8377">
+ <createTable tableName="ROLE_ATTRIBUTE">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="ROLE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="VALUE" type="NVARCHAR(255)"/>
+ </createTable>
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_ROLE_ATTRIBUTE_PK" tableName="ROLE_ATTRIBUTE"/>
+ <addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="ROLE_ATTRIBUTE" constraintName="FK_ROLE_ATTRIBUTE_ID" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
+ <createIndex indexName="IDX_ROLE_ATTRIBUTE" tableName="ROLE_ATTRIBUTE">
+ <column name="ROLE_ID" type="VARCHAR(36)"/>
+ </createIndex>
+ </changeSet>
+
<changeSet author="gideonray@gmail.com" id="4.6.0-KEYCLOAK-8555">
<createIndex tableName="COMPONENT" indexName="IDX_COMPONENT_PROVIDER_TYPE">
<column name="PROVIDER_TYPE" type="VARCHAR(255)"/>
</createIndex>
</changeSet>
-
+
</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index 76aae9d..d888203 100755
--- a/model/jpa/src/main/resources/META-INF/persistence.xml
+++ b/model/jpa/src/main/resources/META-INF/persistence.xml
@@ -31,6 +31,7 @@
<class>org.keycloak.models.jpa.entities.UserFederationProviderEntity</class>
<class>org.keycloak.models.jpa.entities.UserFederationMapperEntity</class>
<class>org.keycloak.models.jpa.entities.RoleEntity</class>
+ <class>org.keycloak.models.jpa.entities.RoleAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>
<class>org.keycloak.models.jpa.entities.MigrationModelEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
diff --git a/server-spi/src/main/java/org/keycloak/models/RoleModel.java b/server-spi/src/main/java/org/keycloak/models/RoleModel.java
index 119539c..d93c94c 100755
--- a/server-spi/src/main/java/org/keycloak/models/RoleModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RoleModel.java
@@ -17,6 +17,9 @@
package org.keycloak.models;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -50,4 +53,15 @@ public interface RoleModel {
boolean hasRole(RoleModel role);
+ void setSingleAttribute(String name, String value);
+
+ void setAttribute(String name, Collection<String> values);
+
+ void removeAttribute(String name);
+
+ String getFirstAttribute(String name);
+
+ List<String> getAttribute(String name);
+
+ Map<String, List<String>> getAttributes();
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 64d3f21..0697c38 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -230,6 +230,18 @@ public class ModelToRepresentation {
rep.setComposite(role.isComposite());
rep.setClientRole(role.isClientRole());
rep.setContainerId(role.getContainerId());
+ rep.setAttributes(role.getAttributes());
+ return rep;
+ }
+
+ public static RoleRepresentation toBriefRepresentation(RoleModel role) {
+ RoleRepresentation rep = new RoleRepresentation();
+ rep.setId(role.getId());
+ rep.setName(role.getName());
+ rep.setDescription(role.getDescription());
+ rep.setComposite(role.isComposite());
+ rep.setClientRole(role.isClientRole());
+ rep.setContainerId(role.getContainerId());
return rep;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 4081661..db66179 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1033,6 +1033,11 @@ public class RepresentationToModel {
public static void createRole(RealmModel newRealm, RoleRepresentation roleRep) {
RoleModel role = roleRep.getId() != null ? newRealm.addRole(roleRep.getId(), roleRep.getName()) : newRealm.addRole(roleRep.getName());
if (roleRep.getDescription() != null) role.setDescription(roleRep.getDescription());
+ if (roleRep.getAttributes() != null) {
+ for (Map.Entry<String, List<String>> attribute : roleRep.getAttributes().entrySet()) {
+ role.setAttribute(attribute.getKey(), attribute.getValue());
+ }
+ }
}
private static void addComposites(RoleModel role, RoleRepresentation roleRep, RealmModel realm) {
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index 78dff9e..39764e5 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -41,10 +41,8 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.OAuthErrorException;
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
index 5e4dd6f..f87bace 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -45,11 +45,9 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriInfo;
import java.util.Arrays;
import java.util.HashMap;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java
index ae7d519..bb658a2 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java
@@ -31,7 +31,6 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.ErrorResponseException;
-import org.keycloak.services.ForbiddenException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -98,7 +97,7 @@ public class ClientRoleMappingsResource {
Set<RoleModel> mappings = user.getClientRoleMappings(client);
List<RoleRepresentation> mapRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : mappings) {
- mapRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ mapRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return mapRep;
}
@@ -121,7 +120,7 @@ public class ClientRoleMappingsResource {
Set<RoleModel> roles = client.getRoles();
List<RoleRepresentation> mapRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roles) {
- if (user.hasRole(roleModel)) mapRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ if (user.hasRole(roleModel)) mapRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return mapRep;
}
@@ -154,7 +153,7 @@ public class ClientRoleMappingsResource {
List<RoleRepresentation> mappings = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roles) {
- mappings.add(ModelToRepresentation.toRepresentation(roleModel));
+ mappings.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return mappings;
}
@@ -202,7 +201,7 @@ public class ClientRoleMappingsResource {
}
auth.roles().requireMapRole(roleModel);
user.deleteRoleMapping(roleModel);
- roles.add(ModelToRepresentation.toRepresentation(roleModel));
+ roles.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
} else {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java
index 8e514b4..cbdf053 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java
@@ -76,7 +76,7 @@ public class ClientScopeEvaluateScopeMappingsResource {
public List<RoleRepresentation> getGrantedScopeMappings() {
return getGrantedRoles().stream().map((RoleModel role) -> {
- return ModelToRepresentation.toRepresentation(role);
+ return ModelToRepresentation.toBriefRepresentation(role);
}).collect(Collectors.toList());
}
@@ -101,7 +101,7 @@ public class ClientScopeEvaluateScopeMappingsResource {
}).map((RoleModel role) -> {
- return ModelToRepresentation.toRepresentation(role);
+ return ModelToRepresentation.toBriefRepresentation(role);
}).collect(Collectors.toList());
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
index 8ad922b..cebd168 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java
@@ -94,7 +94,7 @@ public class RoleContainerResource extends RoleResource {
Set<RoleModel> roleModels = roleContainer.getRoles();
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roleModels) {
- roles.add(ModelToRepresentation.toRepresentation(roleModel));
+ roles.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return roles;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java
index dccad2b..26feaab 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java
@@ -119,7 +119,7 @@ public class RoleMapperResource {
if (realmMappings.size() > 0) {
List<RoleRepresentation> realmRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
- realmRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ realmRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
all.setRealmMappings(realmRep);
}
@@ -136,7 +136,7 @@ public class RoleMapperResource {
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
mappings.setMappings(roles);
for (RoleModel role : roleMappings) {
- roles.add(ModelToRepresentation.toRepresentation(role));
+ roles.add(ModelToRepresentation.toBriefRepresentation(role));
}
appMappings.put(client.getClientId(), mappings);
all.setClientMappings(appMappings);
@@ -161,7 +161,7 @@ public class RoleMapperResource {
Set<RoleModel> realmMappings = roleMapper.getRealmRoleMappings();
List<RoleRepresentation> realmMappingsRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
- realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ realmMappingsRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return realmMappingsRep;
}
@@ -184,7 +184,7 @@ public class RoleMapperResource {
List<RoleRepresentation> realmMappingsRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roles) {
if (roleMapper.hasRole(roleModel)) {
- realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ realmMappingsRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
}
return realmMappingsRep;
@@ -253,7 +253,7 @@ public class RoleMapperResource {
for (RoleModel roleModel : roleModels) {
auth.roles().requireMapRole(roleModel);
roleMapper.deleteRoleMapping(roleModel);
- roles.add(ModelToRepresentation.toRepresentation(roleModel));
+ roles.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
} else {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java
index 3af52c1..c05bfef 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleResource.java
@@ -31,6 +31,7 @@ import javax.ws.rs.core.UriInfo;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -58,6 +59,19 @@ public abstract class RoleResource {
protected void updateRole(RoleRepresentation rep, RoleModel role) {
role.setName(rep.getName());
role.setDescription(rep.getDescription());
+
+ if (rep.getAttributes() != null) {
+ Set<String> attrsToRemove = new HashSet<>(role.getAttributes().keySet());
+ attrsToRemove.removeAll(rep.getAttributes().keySet());
+
+ for (Map.Entry<String, List<String>> attr : rep.getAttributes().entrySet()) {
+ role.setAttribute(attr.getKey(), attr.getValue());
+ }
+
+ for (String attr : attrsToRemove) {
+ role.removeAttribute(attr);
+ }
+ }
}
protected void addComposites(AdminPermissionEvaluator auth, AdminEventBuilder adminEvent, UriInfo uriInfo, List<RoleRepresentation> roles, RoleModel role) {
@@ -84,7 +98,7 @@ public abstract class RoleResource {
Set<RoleRepresentation> composites = new HashSet<RoleRepresentation>(role.getComposites().size());
for (RoleModel composite : role.getComposites()) {
- composites.add(ModelToRepresentation.toRepresentation(composite));
+ composites.add(ModelToRepresentation.toBriefRepresentation(composite));
}
return composites;
}
@@ -95,7 +109,7 @@ public abstract class RoleResource {
Set<RoleRepresentation> composites = new HashSet<RoleRepresentation>(role.getComposites().size());
for (RoleModel composite : role.getComposites()) {
if (composite.getContainer() instanceof RealmModel)
- composites.add(ModelToRepresentation.toRepresentation(composite));
+ composites.add(ModelToRepresentation.toBriefRepresentation(composite));
}
return composites;
}
@@ -106,7 +120,7 @@ public abstract class RoleResource {
Set<RoleRepresentation> composites = new HashSet<RoleRepresentation>(role.getComposites().size());
for (RoleModel composite : role.getComposites()) {
if (composite.getContainer().equals(app))
- composites.add(ModelToRepresentation.toRepresentation(composite));
+ composites.add(ModelToRepresentation.toBriefRepresentation(composite));
}
return composites;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java
index 4f7b5dc..1f2c455 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java
@@ -87,7 +87,7 @@ public class ScopeMappedClientResource {
Set<RoleModel> mappings = KeycloakModelUtils.getClientScopeMappings(scopedClient, scopeContainer); //scopedClient.getClientScopeMappings(client);
List<RoleRepresentation> mapRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : mappings) {
- mapRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ mapRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return mapRep;
}
@@ -165,7 +165,7 @@ public class ScopeMappedClientResource {
for (RoleModel roleModel : roleModels) {
scopeContainer.deleteScopeMapping(roleModel);
- roles.add(ModelToRepresentation.toRepresentation(roleModel));
+ roles.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
} else {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java
index 286e22b..dbf9545 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java
@@ -98,7 +98,7 @@ public class ScopeMappedResource {
if (realmMappings.size() > 0) {
List<RoleRepresentation> realmRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
- realmRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ realmRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
all.setRealmMappings(realmRep);
}
@@ -115,7 +115,7 @@ public class ScopeMappedResource {
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
mappings.setMappings(roles);
for (RoleModel role : roleMappings) {
- roles.add(ModelToRepresentation.toRepresentation(role));
+ roles.add(ModelToRepresentation.toBriefRepresentation(role));
}
clientMappings.put(client.getClientId(), mappings);
all.setClientMappings(clientMappings);
@@ -144,7 +144,7 @@ public class ScopeMappedResource {
Set<RoleModel> realmMappings = scopeContainer.getRealmScopeMappings();
List<RoleRepresentation> realmMappingsRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
- realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel));
+ realmMappingsRep.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return realmMappingsRep;
}
@@ -174,7 +174,7 @@ public class ScopeMappedResource {
for (RoleModel roleModel : roles) {
if (client.hasScope(roleModel)) continue;
if (!auth.roles().canMapClientScope(roleModel)) continue;
- available.add(ModelToRepresentation.toRepresentation(roleModel));
+ available.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return available;
}
@@ -206,7 +206,7 @@ public class ScopeMappedResource {
public static List<RoleRepresentation> getComposite(ScopeContainerModel client, Set<RoleModel> roles) {
List<RoleRepresentation> composite = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roles) {
- if (client.hasScope(roleModel)) composite.add(ModelToRepresentation.toRepresentation(roleModel));
+ if (client.hasScope(roleModel)) composite.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
return composite;
}
@@ -258,7 +258,7 @@ public class ScopeMappedResource {
for (RoleModel roleModel : roleModels) {
scopeContainer.deleteScopeMapping(roleModel);
- roles.add(ModelToRepresentation.toRepresentation(roleModel));
+ roles.add(ModelToRepresentation.toBriefRepresentation(roleModel));
}
} else {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
index ce4539d..085b04d 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
@@ -501,7 +501,7 @@ public class GroupTest extends AbstractGroupTest {
// List realm roles
assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite");
- assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium", "realm-composite-role", "sample-realm-role");
+ assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user", "customer-user-premium", "realm-composite-role", "sample-realm-role", "attribute-role");
assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child");
// List client roles
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java
index ee4326a..4212252 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java
@@ -30,6 +30,7 @@ import org.keycloak.testsuite.util.RoleBuilder;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -151,4 +152,43 @@ public class RoleByIdResourceTest extends AbstractAdminTest {
}
+ @Test
+ public void attributes() {
+ for (String id : ids.values()) {
+ RoleRepresentation role = resource.getRole(id);
+ assertNotNull(role.getAttributes());
+ assertTrue(role.getAttributes().isEmpty());
+
+ // update the role with attributes
+ Map<String, List<String>> attributes = new HashMap<>();
+ List<String> attributeValues = new ArrayList<>();
+ attributeValues.add("value1");
+ attributes.put("key1", attributeValues);
+ attributeValues = new ArrayList<>();
+ attributeValues.add("value2.1");
+ attributeValues.add("value2.2");
+ attributes.put("key2", attributeValues);
+ role.setAttributes(attributes);
+
+ resource.updateRole(id, role);
+ role = resource.getRole(id);
+ assertNotNull(role);
+ Map<String, List<String>> roleAttributes = role.getAttributes();
+ assertNotNull(roleAttributes);
+
+ assertEquals(attributes, roleAttributes);
+
+
+ // delete an attribute
+ attributes.remove("key2");
+ role.setAttributes(attributes);
+ resource.updateRole(id, role);
+ role = resource.getRole(id);
+ assertNotNull(role);
+ roleAttributes = role.getAttributes();
+ assertNotNull(roleAttributes);
+
+ assertEquals(attributes, roleAttributes);
+ }
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
index d7f494f..35a7623 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserStorageRestTest.java
@@ -388,8 +388,8 @@ public class UserStorageRestTest extends AbstractAdminTest {
String id2 = createUserFederationProvider(dummyRep2);
// Assert provider instances available
- assertFederationProvider(userFederation().get(id1).toRepresentation(), id1, id1, "dummy", 2, 1000, 500, 123);
- assertFederationProvider(userFederation().get(id2).toRepresentation(), id2, "dn1", "dummy", 1, -1, -1, -1, "prop1", "prop1Val", "prop2", "true");
+ assertFederationProvider(userFederation().get(id1).toBriefRepresentation(), id1, id1, "dummy", 2, 1000, 500, 123);
+ assertFederationProvider(userFederation().get(id2).toBriefRepresentation(), id2, "dn1", "dummy", 1, -1, -1, -1, "prop1", "prop1Val", "prop2", "true");
// Assert sorted
List<UserFederationProviderRepresentation> providerInstances = userFederation().getProviderInstances();
@@ -411,7 +411,7 @@ public class UserStorageRestTest extends AbstractAdminTest {
@Test (expected = NotFoundException.class)
public void testLookupNotExistentProvider() {
- userFederation().get("not-existent").toRepresentation();
+ userFederation().get("not-existent").toBriefRepresentation();
}
@@ -433,7 +433,7 @@ public class UserStorageRestTest extends AbstractAdminTest {
}
// Assert sync didn't happen
- Assert.assertEquals(-1, userFederation().get(id1).toRepresentation().getLastSync());
+ Assert.assertEquals(-1, userFederation().get(id1).toBriefRepresentation().getLastSync());
// Sync and assert it happened
SynchronizationResultRepresentation syncResult = userFederation().get(id1).syncUsers("triggerFullSync");
@@ -443,7 +443,7 @@ public class UserStorageRestTest extends AbstractAdminTest {
eventRep.put("action", "triggerFullSync");
assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userFederationResourcePath(id1) + "/sync", eventRep, ResourceType.USER_FEDERATION_PROVIDER);
- int fullSyncTime = userFederation().get(id1).toRepresentation().getLastSync();
+ int fullSyncTime = userFederation().get(id1).toBriefRepresentation().getLastSync();
Assert.assertTrue(fullSyncTime > 0);
// Changed sync
@@ -454,7 +454,7 @@ public class UserStorageRestTest extends AbstractAdminTest {
assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userFederationResourcePath(id1) + "/sync", eventRep, ResourceType.USER_FEDERATION_PROVIDER);
Assert.assertEquals("0 imported users, 0 updated users", syncResult.getStatus());
- int changedSyncTime = userFederation().get(id1).toRepresentation().getLastSync();
+ int changedSyncTime = userFederation().get(id1).toBriefRepresentation().getLastSync();
Assert.assertTrue(fullSyncTime + 50 <= changedSyncTime);
// Cleanup
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 266499d..f535aae 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -1368,7 +1368,7 @@ public class UserTest extends AbstractAdminTest {
// List realm roles
assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
- assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium", "realm-composite-role", "sample-realm-role");
+ assertNames(roles.realmLevel().listAvailable(), "admin", "customer-user-premium", "realm-composite-role", "sample-realm-role", "attribute-role");
assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
// List client roles
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
index 1a85895..4a076d0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
@@ -274,6 +274,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
List<ComponentRepresentation> components = adminClient.realm("test").components().query();
KeysMetadataRepresentation keyMetadata = adminClient.realm("test").keys().getKeyMetadata();
String sampleRealmRoleId = adminClient.realm("test").roles().get("sample-realm-role").toRepresentation().getId();
+ Map<String, List<String>> roleAttributes = adminClient.realm("test").roles().get("attribute-role").toRepresentation().getAttributes();
String testAppId = adminClient.realm("test").clients().findByClientId("test-app").get(0).getId();
String sampleClientRoleId = adminClient.realm("test").clients().get(testAppId).roles().get("sample-client-role").toRepresentation().getId();
@@ -309,6 +310,9 @@ public class ExportImportTest extends AbstractKeycloakTest {
String importedSampleRealmRoleId = adminClient.realm("test").roles().get("sample-realm-role").toRepresentation().getId();
assertEquals(sampleRealmRoleId, importedSampleRealmRoleId);
+ Map<String, List<String>> importedRoleAttributes = adminClient.realm("test").roles().get("attribute-role").toRepresentation().getAttributes();
+ assertEquals(roleAttributes, importedRoleAttributes);
+
String importedSampleClientRoleId = adminClient.realm("test").clients().get(testAppId).roles().get("sample-client-role").toRepresentation().getId();
assertEquals(sampleClientRoleId, importedSampleClientRoleId);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
index bbed561..81239fa 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
@@ -386,6 +386,16 @@
"description": "Sample realm role"
},
{
+ "name": "attribute-role",
+ "description": "has attributes assigned",
+ "attributes": {
+ "hello": [
+ "world",
+ "keycloak"
+ ]
+ }
+ },
+ {
"name": "realm-composite-role",
"description": "Realm composite role containing client role",
"composite" : true,
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index 8afc0a8..984b261 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -788,6 +788,24 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RoleDetailCtrl'
})
+ .when('/realms/:realm/roles/:role/role-attributes', {
+ templateUrl : resourceUrl + '/partials/role-attributes.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ role : function(RoleLoader) {
+ return RoleLoader();
+ },
+ roles : function(RoleListLoader) {
+ return RoleListLoader();
+ },
+ clients : function(ClientListLoader) {
+ return ClientListLoader();
+ }
+ },
+ controller : 'RoleDetailCtrl'
+ })
.when('/realms/:realm/roles/:role/users', {
templateUrl : resourceUrl + '/partials/realm-role-users.html',
resolve : {
@@ -943,6 +961,27 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientRoleDetailCtrl'
})
+ .when('/realms/:realm/clients/:client/roles/:role/role-attributes', {
+ templateUrl : resourceUrl + '/partials/client-role-attributes.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ },
+ role : function(ClientRoleLoader) {
+ return ClientRoleLoader();
+ },
+ roles : function(RoleListLoader) {
+ return RoleListLoader();
+ },
+ clients : function(ClientListLoader) {
+ return ClientListLoader();
+ }
+ },
+ controller : 'ClientRoleDetailCtrl'
+ })
.when('/realms/:realm/clients/:client/mappers', {
templateUrl : resourceUrl + '/partials/client-mappers.html',
resolve : {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index a1c1172..4feb6ae 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -685,12 +685,14 @@ module.controller('ClientRoleDetailCtrl', function($scope, realm, client, role,
$scope.changed = $scope.create;
$scope.save = function() {
+ convertAttributeValuesToLists();
if ($scope.create) {
ClientRole.save({
realm: realm.realm,
client : client.id
}, $scope.role, function (data, headers) {
$scope.changed = false;
+ convertAttributeValuesToString($scope.role);
role = angular.copy($scope.role);
ClientRole.get({ realm: realm.realm, client : client.id, role: role.name }, function(role) {
@@ -721,6 +723,34 @@ module.controller('ClientRoleDetailCtrl', function($scope, realm, client, role,
$location.url("/realms/" + realm.realm + "/clients/" + client.id + "/roles");
};
+ $scope.addAttribute = function() {
+ $scope.role.attributes[$scope.newAttribute.key] = $scope.newAttribute.value;
+ delete $scope.newAttribute;
+ }
+
+ $scope.removeAttribute = function(key) {
+ delete $scope.role.attributes[key];
+ }
+
+ function convertAttributeValuesToLists() {
+ var attrs = $scope.role.attributes;
+ for (var attribute in attrs) {
+ if (typeof attrs[attribute] === "string") {
+ var attrVals = attrs[attribute].split("##");
+ attrs[attribute] = attrVals;
+ }
+ }
+ }
+
+ function convertAttributeValuesToString(role) {
+ var attrs = role.attributes;
+ for (var attribute in attrs) {
+ if (typeof attrs[attribute] === "object") {
+ var attrVals = attrs[attribute].join("##");
+ attrs[attribute] = attrVals;
+ }
+ }
+ }
roleControl($scope, realm, role, roles, clients,
ClientRole, RoleById, RoleRealmComposites, RoleClientComposites,
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 3164a0f..381ecd2 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1461,12 +1461,14 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, roles, clients
$scope.changed = $scope.create;
$scope.save = function() {
+ convertAttributeValuesToLists();
console.log('save');
if ($scope.create) {
Role.save({
realm: realm.realm
}, $scope.role, function (data, headers) {
$scope.changed = false;
+ convertAttributeValuesToString($scope.role);
role = angular.copy($scope.role);
Role.get({ realm: realm.realm, role: role.name }, function(role) {
@@ -1488,7 +1490,35 @@ module.controller('RoleDetailCtrl', function($scope, realm, role, roles, clients
$location.url("/realms/" + realm.realm + "/roles");
};
+ $scope.addAttribute = function() {
+ $scope.role.attributes[$scope.newAttribute.key] = $scope.newAttribute.value;
+ delete $scope.newAttribute;
+ }
+
+ $scope.removeAttribute = function(key) {
+ delete $scope.role.attributes[key];
+ }
+
+ function convertAttributeValuesToLists() {
+ var attrs = $scope.role.attributes;
+ for (var attribute in attrs) {
+ if (typeof attrs[attribute] === "string") {
+ var attrVals = attrs[attribute].split("##");
+ attrs[attribute] = attrVals;
+ }
+ }
+ }
+ function convertAttributeValuesToString(role) {
+ var attrs = role.attributes;
+ for (var attribute in attrs) {
+ if (typeof attrs[attribute] === "object") {
+ var attrVals = attrs[attribute].join("##");
+ attrs[attribute] = attrVals;
+ console.log("attribute" + attrVals)
+ }
+ }
+ }
roleControl($scope, realm, role, roles, clients,
ClientRole, RoleById, RoleRealmComposites, RoleClientComposites,
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-role-attributes.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-role-attributes.html
new file mode 100755
index 0000000..47ef416
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-role-attributes.html
@@ -0,0 +1,45 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
+ <li>{{role.name}}</li>
+ </ol>
+
+ <kc-tabs-client-role></kc-tabs-client-role>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!client.access.configure">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>{{:: 'key' | translate}}</th>
+ <th>{{:: 'value' | translate}}</th>
+ <th>{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+
+ <tr ng-repeat="(key, value) in role.attributes | toOrderedMapSortedByKey">
+ <td>{{key}}</td>
+ <td><input ng-model="role.attributes[key]" class="form-control" type="text" name="{{key}}" id="attribute-{{key}}" /></td>
+ <td class="kc-action-cell" data-ng-click="removeAttribute(key)">{{:: 'delete' | translate}}</td>
+ </tr>
+
+ <tr>
+ <td><input ng-model="newAttribute.key" class="form-control" type="text" id="newAttributeKey" /></td>
+ <td><input ng-model="newAttribute.value" class="form-control" type="text" id="newAttributeValue" /></td>
+ <td class="kc-action-cell" data-ng-click="addAttribute()" data-ng-disabled="!newAttribute.key.length || !newAttribute.value.length">{{:: 'add' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="form-group" data-ng-show="client.access.configure">
+ <div class="col-md-12">
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/role-attributes.html b/themes/src/main/resources/theme/base/admin/resources/partials/role-attributes.html
new file mode 100755
index 0000000..b9cbbd7
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/role-attributes.html
@@ -0,0 +1,41 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/roles">{{:: 'roles' | translate}}</a></li>
+ <li>{{role.name}}</li>
+ </ol>
+
+ <kc-tabs-role></kc-tabs-role>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>{{:: 'key' | translate}}</th>
+ <th>{{:: 'value' | translate}}</th>
+ <th>{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="(key, value) in role.attributes | toOrderedMapSortedByKey">
+ <td>{{key}}</td>
+ <td><input ng-model="role.attributes[key]" class="form-control" type="text" name="{{key}}" id="attribute-{{key}}" /></td>
+ <td class="kc-action-cell" data-ng-click="removeAttribute(key)">{{:: 'delete' | translate}}</td>
+ </tr>
+ <tr>
+ <td><input ng-model="newAttribute.key" class="form-control" type="text" id="newAttributeKey" /></td>
+ <td><input ng-model="newAttribute.value" class="form-control" type="text" id="newAttributeValue" /></td>
+ <td class="kc-action-cell" data-ng-click="addAttribute()" data-ng-disabled="!newAttribute.key.length || !newAttribute.value.length">{{:: 'add' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="form-group" data-ng-show="access.manageRealm">
+ <div class="col-md-12">
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html
index c145583..312f3ca 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html
@@ -5,6 +5,7 @@
<ul class="nav nav-tabs" data-ng-show="!create">
<li ng-class="{active: !path[6]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}">{{:: 'details' | translate}}</a></li>
+ <li ng-class="{active: path[6] && path[6] == 'role-attributes'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}/role-attributes">{{:: 'attributes' | translate}}</a></li>
<li ng-class="{active: path[6] && path[6] == 'permissions'}" data-ng-show="serverInfo.featureEnabled('ADMIN_FINE_GRAINED_AUTHZ') && access.manageAuthorization && client.access.configure">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}/permissions">{{:: 'authz-permissions' | translate}}</a>
<kc-tooltip>{{:: 'manage-permissions-role.tooltip' | translate}}</kc-tooltip>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html
index cdf40ec..f00515b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html
@@ -5,6 +5,7 @@
<ul class="nav nav-tabs" data-ng-show="!create">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/roles/{{role.id}}">{{:: 'details' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'role-attributes'}"><a href="#/realms/{{realm.realm}}/roles/{{role.id}}/role-attributes">{{:: 'attributes' | translate}}</a></li>
<li ng-class="{active: path[4] == 'permissions'}" data-ng-show="serverInfo.featureEnabled('ADMIN_FINE_GRAINED_AUTHZ') && access.manageRealm && access.manageAuthorization">
<a href="#/realms/{{realm.realm}}/roles/{{role.id}}/permissions">{{:: 'authz-permissions' | translate}}</a>
<kc-tooltip>{{:: 'manage-permissions-role.tooltip' | translate}}</kc-tooltip>