diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60e251c5e3..dc2ea7707d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,4 +71,4 @@ jobs: - name: Build testsuite run: mvn clean install -B -DskipTests -f testsuite/pom.xml - name: Run base tests - undertow - run: mvn clean install -B -f testsuite/integration-arquillian/tests/base/pom.xml -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map | misc/log/trimmer.sh; exit ${PIPESTATUS[0]} \ No newline at end of file + run: mvn clean install -B -f testsuite/integration-arquillian/tests/base/pom.xml -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map | misc/log/trimmer.sh; exit ${PIPESTATUS[0]} \ No newline at end of file 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 dfec89e635..0dfa7ff159 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,7 +25,6 @@ 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.HashSet; import java.util.List; import java.util.Map; @@ -182,7 +181,7 @@ public class RoleAdapter implements RoleModel { } @Override - public void setAttribute(String name, Collection values) { + public void setAttribute(String name, List values) { getDelegateForUpdate(); updated.setAttribute(name, values); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index 4202327032..a2a66df1b7 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -28,7 +28,6 @@ import org.keycloak.models.jpa.entities.ClientAttributeEntity; import org.keycloak.models.jpa.entities.ClientEntity; import org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity; import org.keycloak.models.jpa.entities.ProtocolMapperEntity; -import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RoleUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -37,8 +36,7 @@ import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Collection; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -46,6 +44,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -66,6 +65,7 @@ public class ClientAdapter implements ClientModel, JpaModel { this.entity = entity; } + @Override public ClientEntity getEntity() { return entity; } @@ -148,7 +148,7 @@ public class ClientAdapter implements ClientModel, JpaModel { @Override public Set getWebOrigins() { - Set result = new HashSet(); + Set result = new HashSet<>(); result.addAll(entity.getWebOrigins()); return result; } @@ -172,7 +172,7 @@ public class ClientAdapter implements ClientModel, JpaModel { @Override public Set getRedirectUris() { - Set result = new HashSet(); + Set result = new HashSet<>(); result.addAll(entity.getRedirectUris()); return result; } @@ -244,21 +244,19 @@ public class ClientAdapter implements ClientModel, JpaModel { @Override public Stream getScopeMappingsStream() { - return getEntity().getScopeMapping().stream() - .map(RoleEntity::getId) + return entity.getScopeMappingIds().stream() .map(realm::getRoleById) .filter(Objects::nonNull); } @Override public void addScopeMapping(RoleModel role) { - RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em); - getEntity().getScopeMapping().add(roleEntity); + entity.getScopeMappingIds().add(role.getId()); } @Override public void deleteScopeMapping(RoleModel role) { - getEntity().getScopeMapping().remove(RoleAdapter.toRoleEntity(role, em)); + entity.getScopeMappingIds().remove(role.getId()); } @Override @@ -684,69 +682,51 @@ public class ClientAdapter implements ClientModel, JpaModel { @Override public Stream getDefaultRolesStream() { - Collection entities = entity.getDefaultRoles(); - if (entities == null) return Stream.empty(); - return entities.stream().map(RoleEntity::getName); + return entity.getDefaultRolesIds().stream().map(this::getRoleNameById); + } + + private String getRoleNameById(String id) { + RoleModel roleById = session.roles().getRoleById(realm, id); + if (roleById == null) { + return null; + } + return roleById.getName(); } @Override public void addDefaultRole(String name) { + if (entity.getDefaultRolesIds().add(getOrAddRoleId(name))) { + em.flush(); + } + } + + private String getOrAddRoleId(String name) { RoleModel role = getRole(name); if (role == null) { role = addRole(name); } - Collection entities = entity.getDefaultRoles(); - for (RoleEntity entity : entities) { - if (entity.getId().equals(role.getId())) { - return; - } - } - RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em); - entities.add(roleEntity); + return role.getId(); } @Override public void updateDefaultRoles(String... defaultRoles) { - Collection entities = entity.getDefaultRoles(); - Set already = new HashSet(); - List remove = new ArrayList<>(); - for (RoleEntity rel : entities) { - if (!contains(rel.getName(), defaultRoles)) { - remove.add(rel); - } else { - already.add(rel.getName()); - } - } - for (RoleEntity entity : remove) { - entities.remove(entity); - } - em.flush(); - for (String roleName : defaultRoles) { - if (!already.contains(roleName)) { - addDefaultRole(roleName); - } - } + Set newDefaultRolesIds = Arrays.stream(defaultRoles) + .map(this::getOrAddRoleId) + .collect(Collectors.toSet()); + entity.getDefaultRolesIds().retainAll(newDefaultRolesIds); + entity.getDefaultRolesIds().addAll(newDefaultRolesIds); em.flush(); } @Override public void removeDefaultRoles(String... defaultRoles) { - Collection entities = entity.getDefaultRoles(); - List remove = new ArrayList(); - for (RoleEntity rel : entities) { - if (contains(rel.getName(), defaultRoles)) { - remove.add(rel); - } - } - for (RoleEntity entity : remove) { - entities.remove(entity); - } + Arrays.stream(defaultRoles) + .map(this::getRole) + .filter(Objects::nonNull) + .forEach(role -> entity.getDefaultRolesIds().remove(role.getId())); em.flush(); } - - - @Override public int getNodeReRegistrationTimeout() { return entity.getNodeReRegistrationTimeout(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java index a44dfe9af1..4d712bee63 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientScopeAdapter.java @@ -216,21 +216,19 @@ public class ClientScopeAdapter implements ClientScopeModel, JpaModel getScopeMappingsStream() { - return getEntity().getScopeMapping().stream() - .map(RoleEntity::getId) + return entity.getScopeMappingIds().stream() .map(realm::getRoleById) .filter(Objects::nonNull); } @Override public void addScopeMapping(RoleModel role) { - RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em); - getEntity().getScopeMapping().add(roleEntity); + entity.getScopeMappingIds().add(role.getId()); } @Override public void deleteScopeMapping(RoleModel role) { - getEntity().getScopeMapping().remove(RoleAdapter.toRoleEntity(role, em)); + entity.getScopeMappingIds().remove(role.getId()); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index b727ad82cc..d543b447f8 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -163,7 +163,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro adapter.removeClientScope(a.getId()); } - removeRoles(adapter); + session.roles().removeRoles(adapter); adapter.getTopLevelGroupsStream().forEach(adapter::removeGroup); @@ -738,8 +738,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro if (client == null) return false; session.users().preRemove(realm, client); - - removeRoles(client); + session.roles().removeRoles(client); ClientEntity clientEntity = em.find(ClientEntity.class, id, LockModeType.PESSIMISTIC_WRITE); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 656c3ec9b9..fa7b81661a 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -368,10 +368,12 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { @Override public void grantToAllUsers(RealmModel realm, RoleModel role) { - int num = em.createNamedQuery("grantRoleToAllUsers") + if (realm.equals(role.isClientRole() ? ((ClientModel)role.getContainer()).getRealm() : (RealmModel)role.getContainer())) { + int num = em.createNamedQuery("grantRoleToAllUsers") .setParameter("realmId", realm.getId()) .setParameter("roleId", role.getId()) .executeUpdate(); + } } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 5c16154d71..9fd2d53e7c 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -33,6 +33,7 @@ import javax.persistence.LockModeType; import javax.persistence.TypedQuery; import java.util.*; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Objects.nonNull; @@ -705,74 +706,47 @@ public class RealmAdapter implements RealmModel, JpaModel { return realm.getRequiredCredentials().stream().map(this::toRequiredCredentialModel); } - @Override public Stream getDefaultRolesStream() { - Collection entities = realm.getDefaultRoles(); - if (entities == null || entities.isEmpty()) return Stream.empty(); - return entities.stream().map(RoleEntity::getName); + return realm.getDefaultRolesIds().stream().map(this::getRoleNameById); + } + + private String getRoleNameById(String id) { + return getRoleById(id).getName(); } @Override public void addDefaultRole(String name) { + if (realm.getDefaultRolesIds().add(getOrAddRoleId(name))) { + em.flush(); + } + } + + private String getOrAddRoleId(String name) { RoleModel role = getRole(name); if (role == null) { role = addRole(name); } - Collection entities = realm.getDefaultRoles(); - for (RoleEntity entity : entities) { - if (entity.getId().equals(role.getId())) { - return; - } - } - RoleEntity roleEntity = RoleAdapter.toRoleEntity(role, em); - entities.add(roleEntity); - em.flush(); - } - - public static boolean contains(String str, String[] array) { - for (String s : array) { - if (str.equals(s)) return true; - } - return false; + return role.getId(); } @Override public void updateDefaultRoles(String[] defaultRoles) { - Collection entities = realm.getDefaultRoles(); - Set already = new HashSet(); - List remove = new ArrayList(); - for (RoleEntity rel : entities) { - if (!contains(rel.getName(), defaultRoles)) { - remove.add(rel); - } else { - already.add(rel.getName()); - } - } - for (RoleEntity entity : remove) { - entities.remove(entity); - } - em.flush(); - for (String roleName : defaultRoles) { - if (!already.contains(roleName)) { - addDefaultRole(roleName); - } - } + Set newDefaultRolesIds = Arrays.stream(defaultRoles) + .map(this::getOrAddRoleId) + .collect(Collectors.toSet()); + realm.getDefaultRolesIds().retainAll(newDefaultRolesIds); + realm.getDefaultRolesIds().addAll(newDefaultRolesIds); em.flush(); } @Override public void removeDefaultRoles(String... defaultRoles) { - Collection entities = realm.getDefaultRoles(); - List remove = new ArrayList(); - for (RoleEntity rel : entities) { - if (contains(rel.getName(), defaultRoles)) { - remove.add(rel); - } - } - for (RoleEntity entity : remove) { - entities.remove(entity); - } + Arrays.stream(defaultRoles) + .map(this::getRole) + .filter(Objects::nonNull) + .map(RoleModel::getId) + .forEach(realm.getDefaultRolesIds()::remove); em.flush(); } 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 d80c8bd217..f97f01d587 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 @@ -28,7 +28,6 @@ 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; @@ -54,6 +53,7 @@ public class RoleAdapter implements RoleModel, JpaModel { this.session = session; } + @Override public RoleEntity getEntity() { return role; } @@ -94,7 +94,7 @@ public class RoleAdapter implements RoleModel, JpaModel { @Override public void addCompositeRole(RoleModel role) { - RoleEntity entity = RoleAdapter.toRoleEntity(role, em); + RoleEntity entity = toRoleEntity(role); for (RoleEntity composite : getEntity().getCompositeRoles()) { if (composite.equals(entity)) return; } @@ -103,13 +103,14 @@ public class RoleAdapter implements RoleModel, JpaModel { @Override public void removeCompositeRole(RoleModel role) { - RoleEntity entity = RoleAdapter.toRoleEntity(role, em); + RoleEntity entity = toRoleEntity(role); getEntity().getCompositeRoles().remove(entity); } @Override public Stream getCompositesStream() { - return getEntity().getCompositeRoles().stream().map(c -> new RoleAdapter(session, realm, em, c)); + Stream composites = getEntity().getCompositeRoles().stream().map(c -> new RoleAdapter(session, realm, em, c)); + return composites.filter(Objects::nonNull); } @Override @@ -133,7 +134,7 @@ public class RoleAdapter implements RoleModel, JpaModel { } @Override - public void setAttribute(String name, Collection values) { + public void setAttribute(String name, List values) { removeAttribute(name); for (String value : values) { @@ -143,10 +144,7 @@ public class RoleAdapter implements RoleModel, JpaModel { @Override public void removeAttribute(String name) { - Collection attributes = role.getAttributes(); - if (attributes == null) { - return; - } + List attributes = role.getAttributes(); Query query = em.createNamedQuery("deleteRoleAttributesByNameAndUser"); query.setParameter("name", name); @@ -156,11 +154,6 @@ public class RoleAdapter implements RoleModel, JpaModel { attributes.removeIf(attribute -> attribute.getName().equals(name)); } - @Override - public String getFirstAttribute(String name) { - return getAttributeStream(name).findFirst().orElse(null); - } - @Override public Stream getAttributeStream(String name) { return role.getAttributes().stream() @@ -185,19 +178,13 @@ public class RoleAdapter implements RoleModel, JpaModel { @Override public String getContainerId() { - if (isClientRole()) return role.getClientId(); - else return realm.getId(); + return isClientRole() ? role.getClientId() : role.getRealmId(); } @Override public RoleContainerModel getContainer() { - if (role.isClientRole()) { - return realm.getClientById(role.getClientId()); - - } else { - return realm; - } + return isClientRole() ? realm.getClientById(role.getClientId()) : realm; } @Override @@ -214,7 +201,7 @@ public class RoleAdapter implements RoleModel, JpaModel { return getId().hashCode(); } - public static RoleEntity toRoleEntity(RoleModel model, EntityManager em) { + private RoleEntity toRoleEntity(RoleModel model) { if (model instanceof RoleAdapter) { return ((RoleAdapter) model).getEntity(); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index dc910db52f..7ecf08b58a 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -29,7 +29,6 @@ import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; import javax.persistence.ManyToOne; import javax.persistence.MapKeyColumn; import javax.persistence.NamedQueries; @@ -154,13 +153,15 @@ public class ClientEntity { @Column(name="NODE_REREG_TIMEOUT") private int nodeReRegistrationTimeout; - @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true) - @JoinTable(name="CLIENT_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="CLIENT_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")}) - Collection defaultRoles; + @ElementCollection + @Column(name="ROLE_ID") + @CollectionTable(name="CLIENT_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="CLIENT_ID")}) + private Set defaultRolesIds; - @OneToMany(fetch = FetchType.LAZY) - @JoinTable(name="SCOPE_MAPPING", joinColumns = { @JoinColumn(name="CLIENT_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")}) - protected Set scopeMapping; + @ElementCollection + @Column(name="ROLE_ID") + @CollectionTable(name="SCOPE_MAPPING", joinColumns = { @JoinColumn(name="CLIENT_ID")}) + private Set scopeMappingIds; @ElementCollection @MapKeyColumn(name="NAME") @@ -375,15 +376,15 @@ public class ClientEntity { this.managementUrl = managementUrl; } - public Collection getDefaultRoles() { - if (defaultRoles == null) { - defaultRoles = new LinkedList<>(); + public Set getDefaultRolesIds() { + if (defaultRolesIds == null) { + defaultRolesIds = new HashSet<>(); } - return defaultRoles; + return defaultRolesIds; } - public void setDefaultRoles(Collection defaultRoles) { - this.defaultRoles = defaultRoles; + public void setDefaultRolesIds(Set defaultRolesIds) { + this.defaultRolesIds = defaultRolesIds; } public boolean isBearerOnly() { @@ -453,15 +454,15 @@ public class ClientEntity { this.registeredNodes = registeredNodes; } - public Set getScopeMapping() { - if (scopeMapping == null) { - scopeMapping = new HashSet<>(); + public Set getScopeMappingIds() { + if (scopeMappingIds == null) { + scopeMappingIds = new HashSet<>(); } - return scopeMapping; + return scopeMappingIds; } - public void setScopeMapping(Set scopeMapping) { - this.scopeMapping = scopeMapping; + public void setScopeMapping(Set scopeMappingIds) { + this.scopeMappingIds = scopeMappingIds; } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java index 728e75b917..5cc1139d8d 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java @@ -26,7 +26,9 @@ import java.util.Set; import javax.persistence.Access; import javax.persistence.AccessType; import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; import javax.persistence.Column; +import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; @@ -69,9 +71,10 @@ public class ClientScopeEntity { @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "clientScope") protected Collection attributes; - @OneToMany(fetch = FetchType.LAZY) - @JoinTable(name="CLIENT_SCOPE_ROLE_MAPPING", joinColumns = { @JoinColumn(name="SCOPE_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")}) - protected Set scopeMapping = new HashSet<>(); + @ElementCollection + @Column(name="ROLE_ID") + @CollectionTable(name="CLIENT_SCOPE_ROLE_MAPPING", joinColumns = { @JoinColumn(name="SCOPE_ID")}) + private Set scopeMappingIds = new HashSet<>(); public RealmEntity getRealm() { return realm; @@ -135,12 +138,12 @@ public class ClientScopeEntity { this.attributes = attributes; } - public Set getScopeMapping() { - return scopeMapping; + public Set getScopeMappingIds() { + return scopeMappingIds; } - public void setScopeMapping(Set scopeMapping) { - this.scopeMapping = scopeMapping; + public void setScopeMappingIds(Set scopeMappingIds) { + this.scopeMappingIds = scopeMappingIds; } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index b6c2b312f2..7f2ab999a2 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -157,9 +157,10 @@ public class RealmEntity { @CollectionTable(name="REALM_SMTP_CONFIG", joinColumns={ @JoinColumn(name="REALM_ID") }) protected Map smtpConfig; - @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true) - @JoinTable(name="REALM_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")}) - protected Collection defaultRoles; + @ElementCollection + @Column(name="ROLE_ID") + @CollectionTable(name="REALM_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="REALM_ID")}) + protected Set defaultRolesIds; @ElementCollection @Column(name="GROUP_ID") @@ -454,15 +455,15 @@ public class RealmEntity { this.smtpConfig = smtpConfig; } - public Collection getDefaultRoles() { - if (defaultRoles == null) { - defaultRoles = new LinkedList<>(); + public Set getDefaultRolesIds() { + if (defaultRolesIds == null) { + defaultRolesIds = new HashSet<>(); } - return defaultRoles; + return defaultRolesIds; } - public void setDefaultRoles(Collection defaultRoles) { - this.defaultRoles = defaultRoles; + public void setDefaultRolesIds(Set defaultRolesIds) { + this.defaultRolesIds = defaultRolesIds; } public Set getDefaultGroupIds() { 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 c4cec5df94..c5c2bc2958 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 @@ -38,7 +38,6 @@ import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; -import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -105,7 +104,7 @@ public class RoleEntity { @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="role") @Fetch(FetchMode.SELECT) @BatchSize(size = 20) - protected Collection attributes; + protected List attributes; public String getId() { return id; @@ -123,14 +122,14 @@ public class RoleEntity { this.realmId = realmId; } - public Collection getAttributes() { + public List getAttributes() { if (attributes == null) { attributes = new LinkedList<>(); } return attributes; } - public void setAttributes(Collection attributes) { + public void setAttributes(List attributes) { this.attributes = attributes; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java index 884ba3d8f3..a75076fa60 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java @@ -42,7 +42,7 @@ import java.io.Serializable; @NamedQuery(name="deleteUserRoleMappingsByRealmAndLink", query="delete from UserRoleMappingEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"), @NamedQuery(name="deleteUserRoleMappingsByRole", query="delete from UserRoleMappingEntity m where m.roleId = :roleId"), @NamedQuery(name="deleteUserRoleMappingsByUser", query="delete from UserRoleMappingEntity m where m.user = :user"), - @NamedQuery(name="grantRoleToAllUsers", query="insert into UserRoleMappingEntity (roleId, user) select role.id, user from RoleEntity role, UserEntity user where role.id = :roleId AND role.realm.id = :realmId AND user.realmId = :realmId") + @NamedQuery(name="grantRoleToAllUsers", query="insert into UserRoleMappingEntity (roleId, user) select :roleId, user from UserEntity user where user.realmId = :realmId") }) @Table(name="USER_ROLE_MAPPING") diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-12.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-12.0.0.xml index b68b8303f2..53921a7feb 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-12.0.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-12.0.0.xml @@ -19,6 +19,11 @@ + + + + + diff --git a/model/map/src/main/java/org/keycloak/models/map/common/StreamUtils.java b/model/map/src/main/java/org/keycloak/models/map/common/StreamUtils.java new file mode 100644 index 0000000000..fe87dea075 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/StreamUtils.java @@ -0,0 +1,201 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.common; + +import java.util.Iterator; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import static java.util.Spliterator.IMMUTABLE; + +/** + * + * @author hmlnarik + */ +public class StreamUtils { + + public static final class Pair { + private final T1 k; + private final T2 v; + + public Pair(T1 k, T2 v) { + this.k = k; + this.v = v; + } + + public T1 getK() { + return k; + } + + public T2 getV() { + return v; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + Objects.hashCode(this.k); + hash = 97 * hash + Objects.hashCode(this.v); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Pair other = (Pair) obj; + if ( ! Objects.equals(this.k, other.k)) { + return false; + } + if ( ! Objects.equals(this.v, other.v)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "[" + k + ", " + v + "]"; + } + } + + public static abstract class AbstractToPairSpliterator implements Spliterator> { + + protected final Iterator streamIterator; + protected final M mapper; + protected Iterator flatMapIterator; + protected K currentKey; + + public AbstractToPairSpliterator(Stream stream, M mapper) { + this.streamIterator = stream.iterator(); + this.mapper = mapper; + } + + protected abstract void nextKey(); + + @Override + public boolean tryAdvance(Consumer> action) { + if (flatMapIterator != null && flatMapIterator.hasNext()) { + action.accept(new Pair<>(currentKey, flatMapIterator.next())); + return true; + } + + nextKey(); + + if (flatMapIterator != null && flatMapIterator.hasNext()) { + action.accept(new Pair<>(currentKey, flatMapIterator.next())); + return true; + } + return false; + } + + @Override + public Spliterator> trySplit() { + return null; + } + + @Override + public long estimateSize() { + return Long.MAX_VALUE; + } + + @Override + public int characteristics() { + return IMMUTABLE; + } + } + + private static class ToPairSpliterator extends AbstractToPairSpliterator>> { + + public ToPairSpliterator(Stream stream, Function> mapper) { + super(stream, mapper); + } + + @Override + protected void nextKey() { + this.flatMapIterator = null; + while (this.flatMapIterator == null && streamIterator.hasNext()) { + currentKey = streamIterator.next(); + final Stream vStream = mapper.apply(currentKey); + this.flatMapIterator = vStream == null ? null : vStream.iterator(); + } + } + } + + private static class IterableToPairSpliterator extends AbstractToPairSpliterator>> { + + public IterableToPairSpliterator(Stream stream, Function> mapper) { + super(stream, mapper); + } + + @Override + protected void nextKey() { + this.flatMapIterator = null; + while (this.flatMapIterator == null && streamIterator.hasNext()) { + currentKey = streamIterator.next(); + final Iterable vStream = mapper.apply(currentKey); + this.flatMapIterator = vStream == null ? null : vStream.iterator(); + } + } + } + + + /** + * Creates a stream of pairs that join two streams. For each element k from the {@code stream} + * and each element v obtained from the stream returned by the {@code mapper} for k, generates + * a stream of pairs (k, v). + *

+ * Effectively performs equivalent of a {@code LEFT INNER JOIN} SQL operation on streams. + * + * @param + * @param + * @param stream + * @param mapper + * @return + */ + public static Stream> leftInnerJoinStream(Stream stream, Function> mapper) { + return StreamSupport.stream(() -> new ToPairSpliterator<>(stream, mapper), IMMUTABLE, false); + } + + /** + * Creates a stream of pairs that join two streams. For each element k from the {@code stream} + * and each element v obtained from the {@code Iterable} returned by the {@code mapper} for k, generates + * a stream of pairs (k, v). + *

+ * Effectively performs equivalent of a {@code LEFT INNER JOIN} SQL operation on streams. + * + * @param + * @param + * @param stream + * @param mapper + * @return + */ + public static Stream> leftInnerJoinIterable(Stream stream, Function> mapper) { + return StreamSupport.stream(() -> new IterableToPairSpliterator<>(stream, mapper), IMMUTABLE, false); + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/role/AbstractRoleEntity.java b/model/map/src/main/java/org/keycloak/models/map/role/AbstractRoleEntity.java new file mode 100644 index 0000000000..310fe27e7b --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/role/AbstractRoleEntity.java @@ -0,0 +1,150 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.role; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.keycloak.models.map.common.AbstractEntity; + +public abstract class AbstractRoleEntity implements AbstractEntity { + + private K id; + private String realmId; + + private String name; + private String description; + private boolean clientRole; + private String clientId; + private Set compositeRoles = new HashSet<>(); + private Map> attributes = new HashMap<>(); + + /** + * Flag signalizing that any of the setters has been meaningfully used. + */ + protected boolean updated; + + protected AbstractRoleEntity() { + this.id = null; + this.realmId = null; + } + + public AbstractRoleEntity(K id, String realmId) { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(realmId, "realmId"); + + this.id = id; + this.realmId = realmId; + } + + @Override + public K getId() { + return this.id; + } + + @Override + public boolean isUpdated() { + return this.updated; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.updated |= ! Objects.equals(this.name, name); + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.updated |= ! Objects.equals(this.description, description); + this.description = description; + } + + public Map> getAttributes() { + return attributes; + } + + public void setAttributes(Map> attributes) { + this.updated |= ! Objects.equals(this.attributes, attributes); + this.attributes = attributes; + } + + public void setAttribute(String name, List values) { + this.updated |= ! Objects.equals(this.attributes.put(name, values), values); + } + + public void removeAttribute(String name) { + this.updated |= this.attributes.remove(name) != null; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.updated |= ! Objects.equals(this.realmId, realmId); + this.realmId = realmId; + } + + public boolean isClientRole() { + return clientRole; + } + + public void setClientRole(boolean clientRole) { + this.updated |= ! Objects.equals(this.clientRole, clientRole); + this.clientRole = clientRole; + } + + public boolean isComposite() { + return ! (compositeRoles == null || compositeRoles.isEmpty()); + } + + public Set getCompositeRoles() { + return compositeRoles; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.updated |= ! Objects.equals(this.clientId, clientId); + this.clientId = clientId; + } + + public void setCompositeRoles(Set compositeRoles) { + this.updated |= ! Objects.equals(this.compositeRoles, compositeRoles); + this.compositeRoles.clear(); + this.compositeRoles.addAll(compositeRoles); + } + + public void addCompositeRole(String roleId) { + this.updated |= this.compositeRoles.add(roleId); + } + + public void removeCompositeRole(String roleId) { + this.updated |= this.compositeRoles.remove(roleId); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/role/AbstractRoleModel.java b/model/map/src/main/java/org/keycloak/models/map/role/AbstractRoleModel.java new file mode 100644 index 0000000000..9b37dfd742 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/role/AbstractRoleModel.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.role; + +import java.util.Objects; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.map.common.AbstractEntity; + +public abstract class AbstractRoleModel implements RoleModel { + + protected final KeycloakSession session; + protected final RealmModel realm; + protected final E entity; + + public AbstractRoleModel(KeycloakSession session, RealmModel realm, E entity) { + Objects.requireNonNull(entity, "entity"); + Objects.requireNonNull(realm, "realm"); + + this.session = session; + this.realm = realm; + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RoleModel)) return false; + + RoleModel that = (RoleModel) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleAdapter.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleAdapter.java new file mode 100644 index 0000000000..26b3748a95 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleAdapter.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.role; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; +import org.jboss.logging.Logger; +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RoleModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.utils.KeycloakModelUtils; + +public class MapRoleAdapter extends AbstractRoleModel implements RoleModel { + + private static final Logger LOG = Logger.getLogger(MapRoleAdapter.class); + + public MapRoleAdapter(KeycloakSession session, RealmModel realm, MapRoleEntity entity) { + super(session, realm, entity); + } + + @Override + public String getName() { + return entity.getName(); + } + + @Override + public String getDescription() { + return entity.getDescription(); + } + + @Override + public void setDescription(String description) { + entity.setDescription(description); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public void setName(String name) { + entity.setName(name); + } + + @Override + public boolean isComposite() { + return ! entity.getCompositeRoles().isEmpty(); + } + + @Override + public Stream getCompositesStream() { + LOG.tracef("%% %s(%s).getCompositesStream():%d - %s", entity.getName(), entity.getId().toString(), entity.getCompositeRoles().size(), getShortStackTrace()); + return entity.getCompositeRoles().stream() + .map(uuid -> session.roles().getRoleById(realm, uuid)) + .filter(Objects::nonNull); + } + + @Override + public void addCompositeRole(RoleModel role) { + LOG.tracef("%s(%s).addCompositeRole(%s(%s))%s", entity.getName(), entity.getId().toString(), role.getName(), role.getId(), getShortStackTrace()); + entity.addCompositeRole(role.getId()); + } + + @Override + public void removeCompositeRole(RoleModel role) { + LOG.tracef("%s(%s).removeCompositeRole(%s(%s))%s", entity.getName(), entity.getId().toString(), role.getName(), role.getId(), getShortStackTrace()); + entity.removeCompositeRole(role.getId()); + } + + @Override + public boolean isClientRole() { + return entity.isClientRole(); + } + + @Override + public String getContainerId() { + return isClientRole() ? entity.getClientId() : entity.getRealmId(); + } + + @Override + public RoleContainerModel getContainer() { + return isClientRole() ? session.clients().getClientById(realm, entity.getClientId()) : realm; + } + + @Override + public void setAttribute(String name, List values) { + entity.setAttribute(name, values); + } + + @Override + public void setSingleAttribute(String name, String value) { + setAttribute(name, Collections.singletonList(value)); + } + + @Override + public void removeAttribute(String name) { + entity.removeAttribute(name); + } + + @Override + public Map> getAttributes() { + return entity.getAttributes(); + } + + @Override + public boolean hasRole(RoleModel role) { + return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>()); + } + + @Override + public Stream getAttributeStream(String name) { + return getAttributes().getOrDefault(name, Collections.EMPTY_LIST).stream(); + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java new file mode 100644 index 0000000000..3ee414f807 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleEntity.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.role; + +import java.util.UUID; + +public class MapRoleEntity extends AbstractRoleEntity { + + protected MapRoleEntity() { + super(); + } + + public MapRoleEntity(UUID id, String realmId) { + super(id, realmId); + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java new file mode 100644 index 0000000000..8b4ec1e8d6 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java @@ -0,0 +1,371 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.role; + +import org.jboss.logging.Logger; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.RealmModel; + +import org.keycloak.models.RoleModel; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.common.Serialization; +import java.util.Comparator; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.keycloak.models.map.storage.MapStorage; +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleProvider; +import org.keycloak.models.map.common.StreamUtils; + +public class MapRoleProvider implements RoleProvider { + + private static final Logger LOG = Logger.getLogger(MapRoleProvider.class); + private static final Predicate ALWAYS_FALSE = role -> { return false; }; + private final KeycloakSession session; + final MapKeycloakTransaction tx; + private final MapStorage roleStore; + + private static final Comparator COMPARE_BY_NAME = new Comparator() { + @Override + public int compare(MapRoleEntity o1, MapRoleEntity o2) { + String r1 = o1 == null ? null : o1.getName(); + String r2 = o2 == null ? null : o2.getName(); + return r1 == r2 ? 0 + : r1 == null ? -1 + : r2 == null ? 1 + : r1.compareTo(r2); + + } + }; + + public MapRoleProvider(KeycloakSession session, MapStorage roleStore) { + this.session = session; + this.roleStore = roleStore; + this.tx = new MapKeycloakTransaction<>(roleStore); + session.getTransactionManager().enlist(tx); + } + + private Function entityToAdapterFunc(RealmModel realm) { + // Clone entity before returning back, to avoid giving away a reference to the live object to the caller + + return origEntity -> new MapRoleAdapter(session, realm, registerEntityForChanges(origEntity)); + } + + private MapRoleEntity registerEntityForChanges(MapRoleEntity origEntity) { + final MapRoleEntity res = Serialization.from(origEntity); + tx.putIfChanged(origEntity.getId(), res, MapRoleEntity::isUpdated); + return res; + } + + private Predicate entityRealmFilter(RealmModel realm) { + if (realm == null || realm.getId() == null) { + return MapRoleProvider.ALWAYS_FALSE; + } + String realmId = realm.getId(); + return entity -> Objects.equals(realmId, entity.getRealmId()); + } + + private Predicate entityClientFilter(ClientModel client) { + if (client == null || client.getId() == null) { + return MapRoleProvider.ALWAYS_FALSE; + } + String clientId = client.getId(); + return entity -> entity.isClientRole() && + Objects.equals(clientId, entity.getClientId()); + } + + private Stream getNotRemovedUpdatedRolesStream(RealmModel realm) { + Stream updatedAndNotRemovedRolesStream = roleStore.entrySet().stream() + .map(tx::getUpdated) // If the role has been removed, tx.get will return null, otherwise it will return me.getValue() + .filter(Objects::nonNull); + return Stream.concat(tx.createdValuesStream(roleStore.keySet()), updatedAndNotRemovedRolesStream) + .filter(entityRealmFilter(realm)); + } + + @Override + public RoleModel addRealmRole(RealmModel realm, String id, String name) { + if (getRealmRole(realm, name) != null) { + throw new ModelDuplicateException("Role exists: " + id); + } + + final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id); + + LOG.tracef("addRealmRole(%s, %s, %s)%s", realm.getName(), id, name, getShortStackTrace()); + + MapRoleEntity entity = new MapRoleEntity(entityId, realm.getId()); + entity.setName(name); + entity.setRealmId(realm.getId()); + if (tx.get(entity.getId(), roleStore::get) != null) { + throw new ModelDuplicateException("Role exists: " + id); + } + tx.putIfAbsent(entity.getId(), entity); + return entityToAdapterFunc(realm).apply(entity); + } + + @Override + public Stream getRealmRolesStream(RealmModel realm, Integer first, Integer max) { + Stream s = getRealmRolesStream(realm); + if (first != null && first >= 0) { + s = s.skip(first); + } + if (max != null && max >= 0) { + s = s.limit(max); + } + return s; + } + + @Override + public Stream getRealmRolesStream(RealmModel realm) { + return getNotRemovedUpdatedRolesStream(realm) + .filter(this::isRealmRole) + .sorted(COMPARE_BY_NAME) + .map(entityToAdapterFunc(realm)); + } + + private boolean isRealmRole(MapRoleEntity role) { + return ! role.isClientRole(); + } + + @Override + public RoleModel addClientRole(ClientModel client, String id, String name) { + if (getClientRole(client, name) != null) { + throw new ModelDuplicateException("Role exists: " + id); + } + + final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id); + + LOG.tracef("addClientRole(%s, %s, %s)%s", client.getClientId(), id, name, getShortStackTrace()); + + MapRoleEntity entity = new MapRoleEntity(entityId, client.getRealm().getId()); + entity.setName(name); + entity.setClientRole(true); + entity.setClientId(client.getId()); + if (tx.get(entity.getId(), roleStore::get) != null) { + throw new ModelDuplicateException("Role exists: " + id); + } + tx.putIfAbsent(entity.getId(), entity); + return entityToAdapterFunc(client.getRealm()).apply(entity); + } + + @Override + public Stream getClientRolesStream(ClientModel client, Integer first, Integer max) { + Stream s = getClientRolesStream(client); + if (first != null && first > 0) { + s = s.skip(first); + } + if (max != null && max >= 0) { + s = s.limit(max); + } + return s; + } + + @Override + public Stream getClientRolesStream(ClientModel client) { + return getNotRemovedUpdatedRolesStream(client.getRealm()) + .filter(entityClientFilter(client)) + .sorted(COMPARE_BY_NAME) + .map(entityToAdapterFunc(client.getRealm())); + } + @Override + public boolean removeRole(RoleModel role) { + LOG.tracef("removeRole(%s(%s))%s", role.getName(), role.getId(), getShortStackTrace()); + + RealmModel realm = role.isClientRole() ? ((ClientModel)role.getContainer()).getRealm() : (RealmModel)role.getContainer(); + + session.users().preRemove(realm, role); + + RoleContainerModel container = role.getContainer(); + if (container.getDefaultRolesStream().anyMatch(r -> Objects.equals(r, role.getName()))) { + container.removeDefaultRoles(role.getName()); + } + + //remove role from realm-roles composites + try (Stream baseStream = getNotRemovedUpdatedRolesStream(realm) + .filter(this::isRealmRole) + .filter(MapRoleEntity::isComposite)) { + + StreamUtils.leftInnerJoinIterable(baseStream, MapRoleEntity::getCompositeRoles) + .filter(pair -> role.getId().equals(pair.getV())) + .collect(Collectors.toSet()) + .forEach(pair -> { + MapRoleEntity origEntity = pair.getK(); + registerEntityForChanges(origEntity); + origEntity.removeCompositeRole(role.getId()); + }); + } + + //remove role from client-roles composites + session.clients().getClientsStream(realm).forEach(client -> { + client.deleteScopeMapping(role); + try (Stream baseStream = getNotRemovedUpdatedRolesStream(client.getRealm()) + .filter(entityClientFilter(client)) + .filter(MapRoleEntity::isComposite)) { + + StreamUtils.leftInnerJoinIterable(baseStream, MapRoleEntity::getCompositeRoles) + .filter(pair -> role.getId().equals(pair.getV())) + .collect(Collectors.toSet()) + .forEach(pair -> { + MapRoleEntity origEntity = pair.getK(); + registerEntityForChanges(origEntity); + origEntity.removeCompositeRole(role.getId()); + }); + } + }); + + session.groups().preRemove(realm, role); + + // TODO: Sending an event should be extracted to store layer + session.getKeycloakSessionFactory().publish(new RoleContainerModel.RoleRemovedEvent() { + @Override + public RoleModel getRole() { + return role; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + // TODO: ^^^^^^^ Up to here + + tx.remove(UUID.fromString(role.getId())); + + return true; + } + + @Override + public void removeRoles(RealmModel realm) { + getRealmRolesStream(realm).forEach(this::removeRole); + } + + @Override + public void removeRoles(ClientModel client) { + getClientRolesStream(client).forEach(this::removeRole); + } + + @Override + public RoleModel getRealmRole(RealmModel realm, String name) { + if (name == null) { + return null; + } + LOG.tracef("getRealmRole(%s, %s)%s", realm.getName(), name, getShortStackTrace()); + + String roleNameLower = name.toLowerCase(); + + String roleId = getNotRemovedUpdatedRolesStream(realm) + .filter(entity -> entity.getName()!= null && Objects.equals(entity.getName().toLowerCase(), roleNameLower)) + .map(entityToAdapterFunc(realm)) + .map(RoleModel::getId) + .findFirst() + .orElse(null); + //we need to go via session.roles() not to bypass cache + return roleId == null ? null : session.roles().getRoleById(realm, roleId); + } + + @Override + public RoleModel getClientRole(ClientModel client, String name) { + if (name == null) { + return null; + } + LOG.tracef("getClientRole(%s, %s)%s", client.getClientId(), name, getShortStackTrace()); + + String roleNameLower = name.toLowerCase(); + + String roleId = getNotRemovedUpdatedRolesStream(client.getRealm()) + .filter(entityClientFilter(client)) + .filter(entity -> entity.getName()!= null && Objects.equals(entity.getName().toLowerCase(), roleNameLower)) + .map(entityToAdapterFunc(client.getRealm())) + .map(RoleModel::getId) + .findFirst() + .orElse(null); + //we need to go via session.roles() not to bypass cache + return roleId == null ? null : session.roles().getRoleById(client.getRealm(), roleId); + } + + @Override + public RoleModel getRoleById(RealmModel realm, String id) { + if (id == null) { + return null; + } + + LOG.tracef("getRoleById(%s, %s)%s", realm.getName(), id, getShortStackTrace()); + + MapRoleEntity entity = tx.get(UUID.fromString(id), roleStore::get); + return (entity == null || ! entityRealmFilter(realm).test(entity)) + ? null + : entityToAdapterFunc(realm).apply(entity); + } + + @Override + public Stream searchForRolesStream(RealmModel realm, String search, Integer first, Integer max) { + if (search == null) { + return Stream.empty(); + } + String searchLower = search.toLowerCase(); + Stream s = getNotRemovedUpdatedRolesStream(realm) + .filter(entity -> + (entity.getName() != null && entity.getName().toLowerCase().contains(searchLower)) || + (entity.getDescription() != null && entity.getDescription().toLowerCase().contains(searchLower)) + ) + .sorted(COMPARE_BY_NAME); + + if (first != null && first > 0) { + s = s.skip(first); + } + if (max != null && max >= 0) { + s = s.limit(max); + } + + return s.map(entityToAdapterFunc(realm)); + } + + @Override + public Stream searchForClientRolesStream(ClientModel client, String search, Integer first, Integer max) { + if (search == null) { + return Stream.empty(); + } + String searchLower = search.toLowerCase(); + Stream s = getNotRemovedUpdatedRolesStream(client.getRealm()) + .filter(entityClientFilter(client)) + .filter(entity -> + (entity.getName() != null && entity.getName().toLowerCase().contains(searchLower)) || + (entity.getDescription() != null && entity.getDescription().toLowerCase().contains(searchLower)) + ) + .sorted(COMPARE_BY_NAME); + + if (first != null && first > 0) { + s = s.skip(first); + } + if (max != null && max >= 0) { + s = s.limit(max); + } + + return s.map(entityToAdapterFunc(client.getRealm())); + } + + @Override + public void close() { + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProviderFactory.java new file mode 100644 index 0000000000..718f45e170 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProviderFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.role; + +import java.util.UUID; +import org.keycloak.models.map.common.AbstractMapProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RoleProvider; +import org.keycloak.models.RoleProviderFactory; +import org.keycloak.models.map.storage.MapStorageProvider; +import org.keycloak.models.map.storage.MapStorage; + +public class MapRoleProviderFactory extends AbstractMapProviderFactory implements RoleProviderFactory { + + private MapStorage store; + + @Override + public void postInit(KeycloakSessionFactory factory) { + MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class); + this.store = sp.getStorage("roles", UUID.class, MapRoleEntity.class); + } + + + @Override + public RoleProvider create(KeycloakSession session) { + return new MapRoleProvider(session, store); + } +} diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.models.RoleProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.models.RoleProviderFactory new file mode 100644 index 0000000000..c11da5a5e0 --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.models.RoleProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2020 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. +# + +org.keycloak.models.map.role.MapRoleProviderFactory diff --git a/model/map/src/test/java/org/keycloak/models/map/common/StreamUtilsTest.java b/model/map/src/test/java/org/keycloak/models/map/common/StreamUtilsTest.java new file mode 100644 index 0000000000..5b27840993 --- /dev/null +++ b/model/map/src/test/java/org/keycloak/models/map/common/StreamUtilsTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.common; + +import org.keycloak.models.map.common.StreamUtils.Pair; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author hmlnarik + */ +public class StreamUtilsTest { + + @Test + public void testLeftInnerJoinStream() { + Stream a = Stream.of(0,1,2,3,4); + Stream[] b = new Stream[] { + Stream.of(1,2), + Stream.of(1,2), + null, + null, + Stream.of(5, 6, 7), + }; + + try (Stream> res = StreamUtils.leftInnerJoinStream(a, n -> b[n])) { + final List> l = res.collect(Collectors.toList()); + assertEquals(l, Arrays.asList( + new Pair<>(0, 1), + new Pair<>(0, 2), + new Pair<>(1, 1), + new Pair<>(1, 2), + new Pair<>(4, 5), + new Pair<>(4, 6), + new Pair<>(4, 7) + )); + } + } + + @Test + public void testLeftInnerJoinIterable() { + Stream a = Stream.of(0,1,2,3,4); + Iterable[] b = new Iterable[] { + Arrays.asList(1,2), + Arrays.asList(1,2), + null, + null, + Arrays.asList(5, 6, 7), + }; + + try (Stream> res = StreamUtils.leftInnerJoinIterable(a, n -> b[n])) { + final List> l = res.collect(Collectors.toList()); + assertEquals(l, Arrays.asList( + new Pair<>(0, 1), + new Pair<>(0, 2), + new Pair<>(1, 1), + new Pair<>(1, 2), + new Pair<>(4, 5), + new Pair<>(4, 6), + new Pair<>(4, 7) + )); + } + } + +} diff --git a/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java b/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java index 67091852fa..b73bd7ee02 100755 --- a/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java @@ -70,15 +70,36 @@ public interface RoleContainerModel { Stream searchForRolesStream(String search, Integer first, Integer max); + /** + * Returns all the default role names of this object. + * @return List of the default role names of this object. Never returns {@code null}. + * @deprecated use the stream variant instead + */ @Deprecated default List getDefaultRoles() { return getDefaultRolesStream().collect(Collectors.toList()); } + /** + * Returns all default role names of this object as a stream. + * @return stream of default role names of this object. Never returns {@code null}. + */ Stream getDefaultRolesStream(); + /** + * Adds a role with given name to default roles of this object. If the role + * doesn't exist a new role is created. + * @param name of the role to be (created and ) added + */ void addDefaultRole(String name); + /** + * Updates default roles of this object. It removes all default roles which + * are not specified by {@code defaultRoles} and adds all which weren't + * present in original default roles. In other words it's the same as calling + * {@code Set.retainAll} and {@code Set.addAll}. + * @param defaultRoles Array of realm roles to be updated + */ default void updateDefaultRoles(String... defaultRoles) { List defaultRolesArray = Arrays.asList(defaultRoles); Collection entities = getDefaultRolesStream().collect(Collectors.toList()); @@ -100,6 +121,10 @@ public interface RoleContainerModel { } } + /** + * Removes default roles from this object according to {@code defaultRoles}. + * @param defaultRoles Role names to be removed from default roles of this object. + */ void removeDefaultRoles(String... defaultRoles); } 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 8c78907bf5..c6553d3430 100755 --- a/server-spi/src/main/java/org/keycloak/models/RoleModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleModel.java @@ -17,7 +17,6 @@ package org.keycloak.models; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -62,11 +61,13 @@ public interface RoleModel { void setSingleAttribute(String name, String value); - void setAttribute(String name, Collection values); + void setAttribute(String name, List values); void removeAttribute(String name); - String getFirstAttribute(String name); + default String getFirstAttribute(String name) { + return getAttributeStream(name).findFirst().orElse(null); + } @Deprecated default List getAttribute(String name) { diff --git a/server-spi/src/main/java/org/keycloak/models/RoleProvider.java b/server-spi/src/main/java/org/keycloak/models/RoleProvider.java index 7b6dde6f56..3a4d92725e 100644 --- a/server-spi/src/main/java/org/keycloak/models/RoleProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleProvider.java @@ -102,7 +102,9 @@ public interface RoleProvider extends Provider, RoleLookupProvider { * @param name String name of the role. * @return Model of the created role. */ - RoleModel addClientRole(ClientModel client, String name); + default RoleModel addClientRole(ClientModel client, String name) { + return addClientRole(client, null, name); + } /** * Adds a client role with given internal ID and {@code name} to the given client. diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserBulkUpdateProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserBulkUpdateProvider.java index f8d56cff9b..843692ddfc 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserBulkUpdateProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserBulkUpdateProvider.java @@ -24,5 +24,12 @@ import org.keycloak.models.RoleModel; * @version $Revision: 1 $ */ public interface UserBulkUpdateProvider { + + /** + * Grants the given role to all users from particular realm. The role has to + * belong to the realm. + * @param realm Realm + * @param role Role to be granted + */ void grantToAllUsers(RealmModel realm, RoleModel role); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java index 812079b393..762b7a8b4f 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java @@ -16,7 +16,6 @@ */ package org.keycloak.testsuite.federation; -import java.util.Collection; import java.util.Map; import java.util.List; import java.util.stream.Stream; @@ -180,7 +179,7 @@ public class HardcodedRoleStorageProvider implements RoleStorageProvider { } @Override - public void setAttribute(String name, Collection values) { + public void setAttribute(String name, List values) { throw new ReadOnlyException("role is read only"); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java index 99d85b3347..5432226ad9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java @@ -65,7 +65,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.assertThat; import static org.keycloak.testsuite.admin.ImpersonationDisabledTest.IMPERSONATION_DISABLED; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; @@ -839,7 +842,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { newRealm.setId("anotherRealm"); newRealm.setEnabled(true); realmClient.realms().create(newRealm); - + ClientRepresentation newClient = new ClientRepresentation(); newClient.setName("newClient"); @@ -851,12 +854,17 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { newClient.setEnabled(true); Response response = realmClient.realm("anotherRealm").clients().create(newClient); Assert.assertEquals(403, response.getStatus()); + response.close(); realmClient.close(); + //creating new client to refresh token realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), "master", "admin", "admin", "fullScopedClient", "618268aa-51e6-4e64-93c4-3c0bc65b8171"); + assertThat(realmClient.realms().findAll().stream().map(RealmRepresentation::getRealm).collect(Collectors.toSet()), + hasItem("anotherRealm")); response = realmClient.realm("anotherRealm").clients().create(newClient); Assert.assertEquals(201, response.getStatus()); + response.close(); } finally { adminClient.realm("anotherRealm").remove(); realmClient.close(); @@ -896,6 +904,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { newClient.setEnabled(true); Response response = adminClient.realm("anotherRealm").clients().create(newClient); Assert.assertEquals(201, response.getStatus()); + response.close(); } finally { adminClient.realm("anotherRealm").remove(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java index 2d0cad6483..5288369f15 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java @@ -39,11 +39,13 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import javax.ws.rs.ClientErrorException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import org.keycloak.testsuite.util.RoleBuilder; /** * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. @@ -87,7 +89,14 @@ public class ClientRolesTest extends AbstractClientTest { assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId, "role1"), role1, ResourceType.CLIENT_ROLE); assertTrue(hasRole(rolesRsc, "role1")); } - + + @Test(expected = ClientErrorException.class) + public void createRoleWithSameName() { + RoleRepresentation role = RoleBuilder.create().name("role-a").build(); + rolesRsc.create(role); + rolesRsc.create(role); + } + @Test public void testRemoveRole() { RoleRepresentation role2 = makeRole("role2"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java index df47144ddf..bf8c4415e9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java @@ -19,7 +19,6 @@ package org.keycloak.testsuite.admin.realm; import org.junit.Before; import org.junit.Test; -import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RolesResource; import org.keycloak.admin.client.resource.UserResource; @@ -47,6 +46,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import javax.ws.rs.ClientErrorException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -55,11 +55,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -153,6 +150,11 @@ public class RealmRolesTest extends AbstractAdminTest { assertFalse(role.isComposite()); } + @Test(expected = ClientErrorException.class) + public void createRoleWithSameName() { + resource.create(RoleBuilder.create().name("role-a").build()); + } + @Test public void updateRole() { RoleRepresentation role = resource.get("role-a").toRepresentation();