Skip to content

Commit

Permalink
KEYCLOAK-18940 Add support for searching composite roles
Browse files Browse the repository at this point in the history
  • Loading branch information
mhajas authored and hmlnarik committed Oct 1, 2021
1 parent 64717f6 commit da0c945
Show file tree
Hide file tree
Showing 17 changed files with 403 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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.MediaType;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -63,6 +64,14 @@ public interface RoleByIdResource {
@Produces(MediaType.APPLICATION_JSON)
Set<RoleRepresentation> getRoleComposites(@PathParam("role-id") String id);

@Path("{role-id}/composites")
@GET
@Produces(MediaType.APPLICATION_JSON)
Set<RoleRepresentation> searchRoleComposites(@PathParam("role-id") String id,
@QueryParam("search") String search,
@QueryParam("first") Integer first,
@QueryParam("max") Integer max);

@Path("{role-id}/composites/realm")
@GET
@Produces(MediaType.APPLICATION_JSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,11 @@ public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, In
return getRoleDelegate().getRealmRolesStream(realm, first, max);
}

@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return getRoleDelegate().getRolesStream(realm, ids, search, first, max);
}

@Override
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
return getRoleDelegate().getClientRolesStream(client, first, max);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ public Stream<RoleModel> getCompositesStream() {
return composites.stream();
}

@Override
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
if (isUpdated()) return updated.getCompositesStream(search, first, max);

return cacheSession.getRoleDelegate().getRolesStream(realm, cached.getComposites().stream(), search, first, max);
}

@Override
public boolean isClientRole() {
return cached instanceof CachedClientRole;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,26 @@ public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, In
return getRolesStream(query, realm, first, max);
}

@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
if (ids == null) return Stream.empty();

TypedQuery<String> query;

if (search == null) {
query = em.createNamedQuery("getRoleIdsFromIdList", String.class);
} else {
query = em.createNamedQuery("getRoleIdsByNameContainingFromIdList", String.class)
.setParameter("search", search);
}

query.setParameter("realm", realm.getId())
.setParameter("ids", ids.collect(Collectors.toList()));

return closing(paginateQuery(query, first, max).getResultStream())
.map(g -> session.roles().getRoleById(realm, g));
}

@Override
public Stream<RoleModel> getClientRolesStream(ClientModel client, Integer first, Integer max) {
TypedQuery<RoleEntity> query = em.createNamedQuery("getClientRoles", RoleEntity.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ public Stream<RoleModel> getCompositesStream() {
return composites.filter(Objects::nonNull);
}

@Override
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
return session.roles().getRolesStream(realm,
getEntity().getCompositeRoles().stream().map(RoleEntity::getId),
search, first, max);
}

@Override
public boolean hasRole(RoleModel role) {
return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
@NamedQuery(name="getRealmRoleByName", query="select role from RoleEntity role where role.clientRole = false and role.name = :name and role.realmId = :realm"),
@NamedQuery(name="getRealmRoleIdByName", query="select role.id from RoleEntity role where role.clientRole = false and role.name = :name and role.realmId = :realm"),
@NamedQuery(name="searchForRealmRoles", query="select role from RoleEntity role where role.clientRole = false and role.realmId = :realm and ( lower(role.name) like :search or lower(role.description) like :search ) order by role.name"),
@NamedQuery(name="getRoleIdsFromIdList", query="select role.id from RoleEntity role where role.realmId = :realm and role.id in :ids order by role.name ASC"),
@NamedQuery(name="getRoleIdsByNameContainingFromIdList", query="select role.id from RoleEntity role where role.realmId = :realm and lower(role.name) like lower(concat('%',:search,'%')) and role.id in :ids order by role.name ASC"),
})

public class RoleEntity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,11 @@ public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, In
return session.roles().getRealmRolesStream(realm, first, max);
}

@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return session.roles().getRolesStream(realm, ids, search, first, max);
}

@Override
@Deprecated
public boolean removeRole(RoleModel role) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,21 @@ public Stream<RoleModel> getCompositesStream() {
.filter(Objects::nonNull);
}

@Override
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
LOG.tracef("%% (%s).getCompositesStream(%s, %d, %d):%d - %s", this, search, first, max, entity.getCompositeRoles().size(), getShortStackTrace());
return session.roles().getRolesStream(realm, entity.getCompositeRoles().stream(), search, first, max);
}

@Override
public void addCompositeRole(RoleModel role) {
LOG.tracef("%s(%s).addCompositeRole(%s(%s))%s", entity.getName(), entity.getId(), role.getName(), role.getId(), getShortStackTrace());
LOG.tracef("(%s).addCompositeRole(%s(%s))%s", this, 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(), role.getName(), role.getId(), getShortStackTrace());
LOG.tracef("(%s).removeCompositeRole(%s(%s))%s", this, role.getName(), role.getId(), getShortStackTrace());
entity.removeCompositeRole(role.getId());
}

Expand Down Expand Up @@ -135,7 +141,7 @@ public Stream<String> getAttributeStream(String name) {

@Override
public String toString() {
return "MapRoleAdapter{" + getId() + '}';
return String.format("%s@%08x", getName(), System.identityHashCode(this));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, In
.map(entityToAdapterFunc(realm));
}

@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
LOG.tracef("getRolesStream(%s, %s, %s, %d, %d)%s", realm, ids, search, first, max, getShortStackTrace());
if (ids == null) return Stream.empty();

ModelCriteriaBuilder<RoleModel> mcb = roleStore.createCriteriaBuilder()
.compare(RoleModel.SearchableFields.ID, Operator.IN, ids)
.compare(RoleModel.SearchableFields.REALM_ID, Operator.EQ, realm.getId());

if (search != null) {
mcb = mcb.compare(RoleModel.SearchableFields.NAME, Operator.ILIKE, "%" + search + "%");
}

return tx.read(withCriteria(mcb).pagination(first, max, RoleModel.SearchableFields.NAME))
.map(entityToAdapterFunc(realm));
}

@Override
public Stream<RoleModel> getRealmRolesStream(RealmModel realm) {
ModelCriteriaBuilder<RoleModel> mcb = roleStore.createCriteriaBuilder()
Expand Down
14 changes: 13 additions & 1 deletion server-spi/src/main/java/org/keycloak/models/RoleModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,19 @@ default Set<RoleModel> getComposites() {
* Returns all composite roles as a stream.
* @return Stream of {@link RoleModel}. Never returns {@code null}.
*/
Stream<RoleModel> getCompositesStream();
default Stream<RoleModel> getCompositesStream() {
return getCompositesStream(null, null, null);
}

/**
* Returns a paginated stream of composite roles of {@code this} role that contain given string in its name.
*
* @param search Case-insensitive search string
* @param first Index of the first result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return A stream of requested roles ordered by the role name
*/
Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max);

boolean isClientRole();

Expand Down
12 changes: 12 additions & 0 deletions server-spi/src/main/java/org/keycloak/models/RoleProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ default Stream<RoleModel> getRealmRolesStream(RealmModel realm) {
*/
Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, Integer max);

/**
* Returns a paginated stream of roles with given ids and given search value in role names.
*
* @param realm Realm. Cannot be {@code null}.
* @param ids Stream of ids. Returns empty {@code Stream} when {@code null}.
* @param search Case-insensitive string to search by role's name or description. Ignored if {@code null}.
* @param first Index of the first result to return. Ignored if negative or {@code null}.
* @param max Maximum number of results to return. Ignored if negative or {@code null}.
* @return Stream of desired roles. Never returns {@code null}.
*/
Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max);

/**
* Removes given realm role from the given realm.
* @param role Role to be removed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
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;
Expand Down Expand Up @@ -176,12 +177,21 @@ public void addComposites(final @PathParam("role-id") String id, List<RoleRepres
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Stream<RoleRepresentation> getRoleComposites(final @PathParam("role-id") String id) {
public Stream<RoleRepresentation> getRoleComposites(final @PathParam("role-id") String id,
final @QueryParam("search") String search,
final @QueryParam("first") Integer first,
final @QueryParam("max") Integer max
) {

if (logger.isDebugEnabled()) logger.debug("*** getRoleComposites: '" + id + "'");
RoleModel role = getRoleModel(id);
auth.roles().requireView(role);
return role.getCompositesStream().map(ModelToRepresentation::toBriefRepresentation);

if (search == null && first == null && max == null) {
return role.getCompositesStream().map(ModelToRepresentation::toBriefRepresentation);
}

return role.getCompositesStream(search, first, max).map(ModelToRepresentation::toBriefRepresentation);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ public Stream<RoleModel> getRealmRolesStream(RealmModel realm, Integer first, In
return session.roleLocalStorage().getRealmRolesStream(realm, first, max);
}

@Override
public Stream<RoleModel> getRolesStream(RealmModel realm, Stream<String> ids, String search, Integer first, Integer max) {
return session.roleLocalStorage().getRolesStream(realm, ids, search, first, max);
}

/**
* Obtaining roles from an external role storage is time-bounded. In case the external role storage
* isn't available at least roles from a local storage are returned. For this purpose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public boolean isComposite() {
}

@Override
public Stream<RoleModel> getCompositesStream() {
public Stream<RoleModel> getCompositesStream(String search, Integer first, Integer max) {
return Stream.empty();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
import java.util.Map;
import java.util.Set;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
Expand Down Expand Up @@ -146,6 +149,21 @@ public void composites() {
Set<RoleRepresentation> clientComposites = resource.getClientRoleComposites(ids.get("role-a"), clientUuid);
Assert.assertNames(clientComposites, "role-c");

composites = resource.searchRoleComposites(ids.get("role-a"), null, null, null);
Assert.assertNames(composites, "role-b", "role-c");

composites = resource.searchRoleComposites(ids.get("role-a"), "b", null, null);
Assert.assertNames(composites, "role-b");

composites = resource.searchRoleComposites(ids.get("role-a"), null, 0, 0);
assertThat(composites, is(empty()));

composites = resource.searchRoleComposites(ids.get("role-a"), null, 0, 1);
Assert.assertNames(composites, "role-b");

composites = resource.searchRoleComposites(ids.get("role-a"), null, 1, 1);
Assert.assertNames(composites, "role-c");

resource.deleteComposites(ids.get("role-a"), l);
assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.roleByIdResourceCompositesPath(ids.get("role-a")), l, ResourceType.REALM_ROLE);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RoleByIdResource;
import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.events.admin.OperationType;
Expand All @@ -39,8 +40,12 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.ws.rs.ClientErrorException;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
Expand Down Expand Up @@ -156,6 +161,47 @@ public void testComposites() {
assertEquals(0, rolesRsc.get("role-a").getRoleComposites().size());
}


@Test
public void testCompositeRolesSearch() {
// Create main-role we will work on
RoleRepresentation mainRole = makeRole("main-role");
rolesRsc.create(mainRole);

RoleResource mainRoleRsc = rolesRsc.get("main-role");
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId, "main-role"), mainRole, ResourceType.CLIENT_ROLE);

// Add composites
List<RoleRepresentation> createdRoles = IntStream.range(0, 20)
.boxed()
.map(i -> makeRole("role" + i))
.peek(rolesRsc::create)
.peek(role -> assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId, role.getName()), role, ResourceType.CLIENT_ROLE))
.map(role -> rolesRsc.get(role.getName()).toRepresentation())
.collect(Collectors.toList());

mainRoleRsc.addComposites(createdRoles);
mainRole = mainRoleRsc.toRepresentation();
RoleByIdResource roleByIdResource = adminClient.realm(getRealmId()).rolesById();

// Search for all composites
Set<RoleRepresentation> foundRoles = roleByIdResource.getRoleComposites(mainRole.getId());
assertThat(foundRoles, hasSize(createdRoles.size()));

// Search paginated composites
foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), null, 0, 10);
assertThat(foundRoles, hasSize(10));

// Search for composites by string role1 (should be role1, role10-role19) without pagination
foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), "role1", null, null);
assertThat(foundRoles, hasSize(11));

// Search for role1 with pagination
foundRoles.forEach(System.out::println);
foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), "role1", 5, 5);
assertThat(foundRoles, hasSize(5));
}

@Test
public void usersInRole() {
String clientID = clientRsc.toRepresentation().getId();
Expand Down
Loading

0 comments on commit da0c945

Please sign in to comment.