KEYCLOAK-8377 Role Attributes

This commit is contained in:
Graser Leon 2018-10-05 14:38:33 +00:00 committed by Marek Posolda
parent 460cdf4508
commit 9ef4c7fffd
33 changed files with 641 additions and 45 deletions

View file

@ -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;
}
}

View file

@ -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());
}
}
@ -167,6 +173,59 @@ public class RoleAdapter implements RoleModel {
return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>());
}
@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;

View file

@ -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);
}
}

View file

@ -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());
}

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -223,6 +223,18 @@ public class ModelToRepresentation {
}
public static RoleRepresentation toRepresentation(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());
rep.setAttributes(role.getAttributes());
return rep;
}
public static RoleRepresentation toBriefRepresentation(RoleModel role) {
RoleRepresentation rep = new RoleRepresentation();
rep.setId(role.getId());
rep.setName(role.getName());

View file

@ -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) {

View file

@ -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();
}

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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());
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -385,6 +385,16 @@
"name": "sample-realm-role",
"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",

View file

@ -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 : {

View file

@ -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,

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>