Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.keycloak.models.jpa.entities.UserConsentEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
Expand Down Expand Up @@ -498,9 +499,7 @@ public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel grou

@Override
public Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role) {
TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
query.setParameter("roleId", role.getId());
return closing(query.getResultStream().map(entity -> new UserAdapter(session, realm, em, entity)));
return getRoleMembersStream(realm, role, -1, -1);
}

@Override
Expand Down Expand Up @@ -776,8 +775,21 @@ public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel grou

@Override
public Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
query.setParameter("roleId", role.getId());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<UserEntity> cq = cb.createQuery(UserEntity.class);
Root<UserRoleMappingEntity> userRoleMapping = cq.from(UserRoleMappingEntity.class);
Root<UserEntity> user = cq.from(UserEntity.class);

List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(userRoleMapping.get("roleId"), role.getId()));
predicates.add(cb.equal(userRoleMapping.get("user"), user));
predicates.addAll(AdminPermissionsSchema.SCHEMA.applyAuthorizationFilters(session, AdminPermissionsSchema.USERS, this, realm, cb, cq, user));

cq.select(user)
.where(predicates.toArray(Predicate[]::new))
.orderBy(cb.asc(user.get("username")));

TypedQuery<UserEntity> query = em.createQuery(cq);

final UserProvider users = session.users();
return closing(paginateQuery(query, firstResult, maxResults).getResultStream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="usersInRole", query="select u from UserRoleMappingEntity m, UserEntity u where m.roleId=:roleId and u=m.user order by u.username"),
@NamedQuery(name="userHasRole", query="select m from UserRoleMappingEntity m where m.user = :user and m.roleId = :roleId"),
@NamedQuery(name="userRoleMappings", query="select m from UserRoleMappingEntity m where m.user = :user"),
@NamedQuery(name="userRoleMappingIds", query="select m.roleId from UserRoleMappingEntity m where m.user = :user"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ public SessionsResource(KeycloakSession session, RealmModel realm, AdminPermissi
)}
)
public Stream<SessionRepresentation> realmSessions(@QueryParam("type") @DefaultValue("ALL") final SessionType type,
@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first")
@DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max) {
@QueryParam("search") @DefaultValue("") final String search,
@QueryParam("first") @DefaultValue("0") int first,
@QueryParam("max") @DefaultValue("10") int max) {
auth.realm().requireViewRealm();

Stream<ClientIdSessionType> sessionIdStream = Stream.<ClientIdSessionType>builder().build();
Expand Down Expand Up @@ -115,8 +116,9 @@ public Stream<SessionRepresentation> realmSessions(@QueryParam("type") @DefaultV
)
public Stream<SessionRepresentation> clientSessions(@QueryParam("clientId") final String clientId,
@QueryParam("type") @DefaultValue("ALL") final SessionType type,
@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first")
@DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max) {
@QueryParam("search") @DefaultValue("") final String search,
@QueryParam("first") @DefaultValue("0") int first,
@QueryParam("max") @DefaultValue("10") int max) {
ClientModel clientModel = realm.getClientById(clientId);
auth.clients().requireView(clientModel);

Expand All @@ -134,7 +136,10 @@ public Stream<SessionRepresentation> clientSessions(@QueryParam("clientId") fina
return applySearch(search, result).distinct().skip(first).limit(max);
}

private static Stream<SessionRepresentation> applySearch(String search, Stream<SessionRepresentation> result) {
private Stream<SessionRepresentation> applySearch(String search, Stream<SessionRepresentation> result) {
result = result.filter(sessionRep ->
auth.users().canView(session.users().getUserById(realm, sessionRep.getUserId()))
);
if (!StringUtil.isBlank(search)) {
String searchTrimmed = search.trim();
result = result.filter(s -> s.getUsername().contains(searchTrimmed) || s.getIpAddress().contains(searchTrimmed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,8 @@ public Stream<UserRepresentation> getUsersInRole(final @Parameter(description =
@Parameter(description = "maximum number of results to return. Ignored if negative or {@code null}.") @QueryParam("max") Integer maxResults) {

auth.roles().requireView(roleContainer);
auth.users().requireQuery();

firstResult = firstResult != null ? firstResult : 0;
maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public UserConfigBuilder email(String email) {
return this;
}

public UserConfigBuilder firstName(String firstName) {
rep.setFirstName(firstName);
return this;
}

public UserConfigBuilder lastName(String lastName) {
rep.setLastName(lastName);
return this;
}

public UserConfigBuilder emailVerified(boolean verified) {
rep.setEmailVerified(verified);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.tests.admin.authz.fgap;

import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
Expand All @@ -34,13 +35,22 @@
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.BearerAuthFilter;
import org.keycloak.admin.client.resource.RolePoliciesResource;
import org.keycloak.admin.ui.rest.model.SessionRepresentation;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
Expand All @@ -53,8 +63,12 @@
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.InjectKeycloakUrls;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedClient;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.testframework.server.KeycloakUrls;
import org.keycloak.testframework.util.ApiUtil;
import org.keycloak.testframework.realm.RoleConfigBuilder;

Expand All @@ -64,6 +78,12 @@ public class UserResourceTypeFilteringTest extends AbstractPermissionTest {
@InjectAdminClient(mode = InjectAdminClient.Mode.MANAGED_REALM, client = "myclient", user = "myadmin")
Keycloak realmAdminClient;

@InjectKeycloakUrls
KeycloakUrls keycloakUrls;

@InjectClient(ref = "test_client")
ManagedClient testClient;

private final String usersType = AdminPermissionsSchema.USERS.getType();

@BeforeEach
Expand Down Expand Up @@ -402,4 +422,106 @@ public void testViewUserUsingRoleInheritedFromCompositeRole() {
assertFalse(search.isEmpty());
assertEquals(1, search.size());
}

@Test
public void testSessionEndpointRespectsUserViewPermission() {
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
String clientUuid = realm.admin().clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0).getId();
RoleRepresentation viewRealmRole = realm.admin().clients().get(clientUuid).roles().get(AdminRoles.VIEW_REALM).toRepresentation();

// create users
for (int i = 0; i < 4; i++) {
String userId = ApiUtil.handleCreatedResponse(realm.admin().users().create(UserConfigBuilder.create()
.username("user" + i)
.password("password")
.firstName("user")
.lastName(Integer.toString(i))
.email("user" + i + "@test")
.build()));
// assign view-realm role to user to be able to access the server info endpoint (to create session)
realm.admin().users().get(userId).roles().clientLevel(clientUuid).add(List.of(viewRealmRole));
}

// grant permission to view user1 and user2 to myadmin
UserPolicyRepresentation policy = createUserPolicy(realm, client, "Myadmin user policy", myadmin.getId());
Set<String> allowedUsers = Set.of("user1", "user2");
createPermission(client, allowedUsers, usersType, Set.of(VIEW), policy);

// assign view-realm role to myadmin so that the user can access the sessions endpoint
realm.admin().users().get(myadmin.getId()).roles().clientLevel(clientUuid).add(List.of(viewRealmRole));
realm.cleanup().add(r -> r.users().get(myadmin.getId()).roles().clientLevel(clientUuid).remove(List.of(viewRealmRole)));

// Create sessions for user1, user2 and user3
Client httpClient = Keycloak.getClientProvider().newRestEasyClient(null, null, true);;
List<Keycloak> keycloakInstances = List.of();
try {
keycloakInstances = Stream.of("user1", "user2", "user3")
.map(username -> KeycloakBuilder.builder()
.serverUrl(keycloakUrls.getBaseUrl().toString())
.realm(realm.getName())
.grantType(OAuth2Constants.PASSWORD)
.clientId(Constants.ADMIN_CLI_CLIENT_ID)
.username(username)
.password("password")
.build())
.peek(kc -> kc.serverInfo().getInfo()) // get server info to create the session
.toList();

WebTarget target = httpClient.target(keycloakUrls.getBaseUrl().toString())
.path("admin")
.path("realms")
.path(realm.getName())
.path("ui-ext")
.path("sessions")
.register(new BearerAuthFilter(realmAdminClient.tokenManager()));

Response response = target.request(MediaType.APPLICATION_JSON).get();

assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));
List<String> sessions = response.readEntity(new GenericType<List<SessionRepresentation>>() {}).stream().map(SessionRepresentation::getUsername).toList();
assertThat(sessions, hasSize(allowedUsers.size()));
assertThat(sessions, hasItems(allowedUsers.toArray(new String[0])));
} finally {
//close http client
httpClient.close();
//close keycloak instances
keycloakInstances.forEach(Keycloak::close);
}
}

@Test
public void testRoleMemberFilteringByViewPermission() {
// Create client role
RoleRepresentation role = new RoleRepresentation();
role.setName("test_role");
realm.admin().clients().get(testClient.getId()).roles().create(role);
role = realm.admin().clients().get(testClient.getId()).roles().get(role.getName()).toRepresentation();
realm.cleanup().add(r -> r.roles().deleteRole("test_role"));

// assign role to users
for (String username : List.of("user_x", "user_y", "user_z")) {
String userId = ApiUtil.handleCreatedResponse(realm.admin().users().create(UserConfigBuilder.create()
.username(username)
.password("password")
.firstName("user")
.lastName(username)
.email(username + "@test")
.build()));
realm.admin().users().get(userId).roles().clientLevel(testClient.getId()).add(List.of(role));
realm.cleanup().add(r -> r.users().delete(userId));
}

// Grant myadmin permission to view user_x and user_y, and to view the test client
UserPolicyRepresentation policy = createUserPolicy(realm, client, "Myadmin user policy", realm.admin().users().search("myadmin").get(0).getId());
Set<String> allowedUsers = Set.of("user_x", "user_y");
createPermission(client, allowedUsers, AdminPermissionsSchema.USERS.getType(), Set.of(AdminPermissionsSchema.VIEW), policy);
createPermission(client, Set.of(testClient.getId()), AdminPermissionsSchema.CLIENTS.getType(), Set.of(AdminPermissionsSchema.VIEW), policy);

// Query role members as myadmin
List<String> roleMembers = realmAdminClient.realm(realm.getName()).clients().get(testClient.getId()).roles().get(role.getName()).getUserMembers().stream().map(UserRepresentation::getUsername).toList();

// Assert only permitted users are returned as role members
assertThat(roleMembers, hasSize(allowedUsers.size()));
assertThat(roleMembers, hasItems(allowedUsers.toArray(new String[0])));
}
}
Loading