keycloak-memoizeit

KEYCLOAK-8377 Role Attributes

10/5/2018 11:38:33 AM

Changes

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>