Skip to content

Commit

Permalink
AAE-19797 Make user type parameterized in search query (#1340)
Browse files Browse the repository at this point in the history
* AAE-19797 add user types to UserSearchParams

* AAE-19797 added UserType enum

* AAE-19797 KeycloakManagementService implementation to retrieve service accounts

* AAE-19797 Param validation, exception handling and controller IT

* AAE-19797 fix sonar issues

* AAE-19797 fix test

* AAE-19797 suppress assert warning
  • Loading branch information
tom-dal authored Feb 2, 2024
1 parent b18bd97 commit 6732338
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.activiti.cloud.identity.IdentityManagementService;
import org.activiti.cloud.identity.IdentityService;
import org.activiti.cloud.identity.UserSearchParams;
import org.activiti.cloud.identity.UserTypeSearchParam;
import org.activiti.cloud.identity.exceptions.IdentityInvalidApplicationException;
import org.activiti.cloud.identity.exceptions.IdentityInvalidGroupException;
import org.activiti.cloud.identity.exceptions.IdentityInvalidGroupRoleException;
Expand All @@ -55,6 +56,8 @@ public class KeycloakManagementService implements IdentityManagementService, Ide
public static final int PAGE_START = 0;
public static final int PAGE_SIZE = 50;

public static final UserTypeSearchParam DEFAULT_USERTYPE = UserTypeSearchParam.INTERACTIVE;

private final KeycloakClient keycloakClient;

public KeycloakManagementService(KeycloakClient keycloakClient) {
Expand All @@ -64,7 +67,10 @@ public KeycloakManagementService(KeycloakClient keycloakClient) {
@Override
public List<User> findUsers(UserSearchParams userSearchParams) {
List<User> users = ObjectUtils.isEmpty(userSearchParams.getGroups())
? searchUsers(userSearchParams.getSearchKey())
? searchUsers(
userSearchParams.getSearchKey(),
userSearchParams.getType() == null ? DEFAULT_USERTYPE : userSearchParams.getType()
)
: searchUsers(userSearchParams.getGroups(), userSearchParams.getSearchKey());

if (!StringUtils.isEmpty(userSearchParams.getApplication())) {
Expand All @@ -74,6 +80,15 @@ public List<User> findUsers(UserSearchParams userSearchParams) {
}
}

private List<User> searchUsers(String searchKey, UserTypeSearchParam userType) {
return switch (userType) {
//UserType=INTERACTIVE: search only users
case INTERACTIVE -> searchUsers(searchKey);
//UserType=ALL: search both users and service accounts. Due to Keycloak search params behavior, search must be done by username.
case ALL -> searchUsersByUsername(searchKey);
};
}

private List<User> searchUsers(String searchKey) {
return keycloakClient
.searchUsers(searchKey, PAGE_START, PAGE_SIZE)
Expand All @@ -82,6 +97,14 @@ private List<User> searchUsers(String searchKey) {
.collect(Collectors.toList());
}

private List<User> searchUsersByUsername(String searchKey) {
return keycloakClient
.searchUsersByUsername(searchKey)
.stream()
.map(KeycloakUserToUser::toUser)
.collect(Collectors.toList());
}

private List<User> searchUsers(Set<String> groups, String searchKey) {
Predicate<User> maybeMatchSearchKey = user ->
!StringUtils.isEmpty(searchKey)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2017-2020 Alfresco Software, Ltd.
*
* 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.activiti.cloud.services.identity.keycloak;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import org.activiti.cloud.identity.UserSearchParams;
import org.activiti.cloud.identity.UserTypeSearchParam;
import org.activiti.cloud.identity.model.User;
import org.activiti.cloud.services.test.containers.KeycloakContainerApplicationInitializer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest(
classes = { KeycloakClientApplication.class },
properties = {
"keycloak.realm=activiti",
"keycloak.use-resource-role-mappings=false",
"identity.client.cache.cacheExpireAfterWrite=PT5s",
}
)
@ContextConfiguration(initializers = { KeycloakContainerApplicationInitializer.class })
class KeycloakManagementServiceIT {

@Autowired
private KeycloakManagementService keycloakManagementService;

@Test
void should_Not_RetrieveServiceAccounts_WhenUserTypeSearchParamIsInteractive() {
UserSearchParams searchParams = new UserSearchParams();
searchParams.setSearch("");
searchParams.setType(UserTypeSearchParam.INTERACTIVE);

List<User> users = keycloakManagementService.findUsers(searchParams);

assertThat(users).isNotEmpty();
assertThat(users).noneMatch(user -> user.getUsername().startsWith("service-account"));
}

@Test
void should_Not_RetrieveServiceAccounts_WhenUserTypeSearchParamIsNull() {
UserSearchParams searchParams = new UserSearchParams();
searchParams.setSearch("");

assertThat(searchParams.getType()).isNull();

List<User> users = keycloakManagementService.findUsers(searchParams);

assertThat(users).isNotEmpty();
assertThat(users).noneMatch(user -> user.getUsername().startsWith("service-account"));
}

@Test
void should_RetrieveUsersAndServiceAccounts_WhenUserTypeSearchParamIsAll() {
UserSearchParams searchParams = new UserSearchParams();
searchParams.setSearch("");
searchParams.setType(UserTypeSearchParam.INTERACTIVE);

List<User> justUsers = keycloakManagementService.findUsers(searchParams);

searchParams.setType(UserTypeSearchParam.ALL);
List<User> usersAndServiceAccounts = keycloakManagementService.findUsers(searchParams);

assertThat(usersAndServiceAccounts.size()).isGreaterThan(justUsers.size());
assertThat(usersAndServiceAccounts)
.allMatch(element -> element.getUsername().startsWith("service-account") ^ justUsers.contains(element));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
*/
package org.activiti.cloud.services.identity.keycloak;

import static org.activiti.cloud.services.identity.keycloak.KeycloakManagementService.PAGE_SIZE;
import static org.activiti.cloud.services.identity.keycloak.KeycloakManagementService.PAGE_START;
import static org.activiti.cloud.services.identity.keycloak.KeycloakManagementService.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.AssertionsForClassTypes.tuple;
Expand All @@ -35,6 +34,7 @@
import java.util.stream.Stream;
import org.activiti.cloud.identity.GroupSearchParams;
import org.activiti.cloud.identity.UserSearchParams;
import org.activiti.cloud.identity.UserTypeSearchParam;
import org.activiti.cloud.identity.exceptions.IdentityInvalidApplicationException;
import org.activiti.cloud.identity.exceptions.IdentityInvalidGroupException;
import org.activiti.cloud.identity.exceptions.IdentityInvalidGroupRoleException;
Expand Down Expand Up @@ -109,9 +109,13 @@ public void setUpData() {
roleB.setId("b");

userOne.setId("one");
userOne.setUsername("one");
userTwo.setId("two");
userTwo.setUsername("two");
userThree.setId("three");
userThree.setUsername("three");
userFour.setId("four");
userFour.setUsername("four");

groupOne.setId("one");
groupOne.setName("groupOne");
Expand Down Expand Up @@ -297,7 +301,7 @@ void should_returnEmptyListOfUsers_when_groupProvidedDoesNotExist() {

List<User> users = keycloakManagementService.findUsers(userSearchParams);

assertThat(users.size()).isEqualTo(0);
assertThat(users.size()).isZero();
}

@Test
Expand Down Expand Up @@ -370,7 +374,7 @@ void should_returnGroups_when_searchingUsingApplicationAndRoles() {
}

@Test
public void should_throwInvalidApplicationException_When_AddingPermissionAndApplicationIsInvalid() {
void should_throwInvalidApplicationException_When_AddingPermissionAndApplicationIsInvalid() {
String expectedMessage = "Invalid Security data: application {fakeClient} is invalid or doesn't exist";

IdentityInvalidApplicationException exception = assertThrows(
Expand Down Expand Up @@ -407,7 +411,7 @@ void should_throwIdentityInvalidRoleException_when_addingApplicationPermissionsW
void should_throwIdentityInvalidUserException_when_addingApplicationPermissionsWithInvalidUser() {
String expectedMessage = "Invalid Security data: user {fakeUser} is invalid or doesn't exist";
setUpClient();
when(keycloakClient.searchUsers(eq("fakeUser"), eq(0), eq(50))).thenReturn(Collections.emptyList());
when(keycloakClient.searchUsers("fakeUser", 0, 50)).thenReturn(Collections.emptyList());
when(keycloakClient.getClientRoles(clientOne.getId())).thenReturn(List.of(keycloakRoleA));

SecurityRequestBodyRepresentation securityRequestBodyRepresentation = new SecurityRequestBodyRepresentation();
Expand All @@ -431,7 +435,7 @@ void should_throwIdentityInvalidUserRoleException_when_addingApplicationPermissi
String expectedMessage = "Invalid Security data: role {a} can't be assigned to user {userOne}";
setUpClient();
setUpUsersRealmRoles();
when(keycloakClient.searchUsers(eq("userOne"), eq(0), eq(50))).thenReturn(List.of(kUserOne));
when(keycloakClient.searchUsers("userOne", 0, 50)).thenReturn(List.of(kUserOne));
when(keycloakClient.getClientRoles(clientOne.getId())).thenReturn(List.of(keycloakRoleA));

SecurityRequestBodyRepresentation securityRequestBodyRepresentation = new SecurityRequestBodyRepresentation();
Expand All @@ -454,7 +458,7 @@ void should_throwIdentityInvalidUserRoleException_when_addingApplicationPermissi
void should_addApplicationPermissionsToUsers() {
setUpClient();
setUpUsersRealmRoles();
when(keycloakClient.searchUsers(eq("userOne"), eq(0), eq(50))).thenReturn(List.of(kUserOne));
when(keycloakClient.searchUsers("userOne", 0, 50)).thenReturn(List.of(kUserOne));
when(keycloakClient.getClientRoles(clientOne.getId())).thenReturn(List.of(keycloakRoleB));

SecurityRequestBodyRepresentation securityRequestBodyRepresentation = new SecurityRequestBodyRepresentation();
Expand All @@ -472,8 +476,8 @@ void should_notAddApplicationPermissionsToUsers_whenGroupIsInvalid() {
String expectedMessage = "Invalid Security data: group {fakeGroup} is invalid or doesn't exist";
setUpClient();
setUpUsersRealmRoles();
when(keycloakClient.searchUsers(eq("userOne"), eq(0), eq(50))).thenReturn(List.of(kUserOne));
when(keycloakClient.searchGroups(eq("fakeGroup"), eq(0), eq(50))).thenReturn(Collections.emptyList());
when(keycloakClient.searchUsers("userOne", 0, 50)).thenReturn(List.of(kUserOne));
when(keycloakClient.searchGroups("fakeGroup", 0, 50)).thenReturn(Collections.emptyList());
when(keycloakClient.getClientRoles(clientOne.getId())).thenReturn(List.of(keycloakRoleB));

SecurityRequestBodyRepresentation securityRequestBodyRepresentation = new SecurityRequestBodyRepresentation();
Expand All @@ -500,7 +504,7 @@ void should_notAddApplicationPermissionsToUsers_whenGroupIsInvalid() {
void should_throwIdentityInvalidGroupException_when_addingApplicationPermissionsWithInvalidGroup() {
String expectedMessage = "Invalid Security data: group {fakeGroup} is invalid or doesn't exist";
setUpClient();
when(keycloakClient.searchGroups(eq("fakeGroup"), eq(0), eq(50))).thenReturn(Collections.emptyList());
when(keycloakClient.searchGroups("fakeGroup", 0, 50)).thenReturn(Collections.emptyList());
when(keycloakClient.getClientRoles(clientOne.getId())).thenReturn(List.of(keycloakRoleA));

SecurityRequestBodyRepresentation securityRequestBodyRepresentation = new SecurityRequestBodyRepresentation();
Expand All @@ -525,7 +529,7 @@ void should_throwIdentityInvalidGroupRoleException_when_addingApplicationPermiss
setUpClient();
setUpGroupsRealmRoles();

when(keycloakClient.searchGroups(eq("groupOne"), eq(0), eq(50))).thenReturn(List.of(kGroupOne));
when(keycloakClient.searchGroups("groupOne", 0, 50)).thenReturn(List.of(kGroupOne));
when(keycloakClient.getClientRoles(clientOne.getId())).thenReturn(List.of(keycloakRoleA));

SecurityRequestBodyRepresentation securityRequestBodyRepresentation = new SecurityRequestBodyRepresentation();
Expand All @@ -548,7 +552,7 @@ void should_throwIdentityInvalidGroupRoleException_when_addingApplicationPermiss
void should_addApplicationPermissionsToGroup() {
setUpClient();
setUpGroupsRealmRoles();
when(keycloakClient.searchGroups(eq("groupOne"), eq(0), eq(50))).thenReturn(List.of(kGroupOne));
when(keycloakClient.searchGroups("groupOne", 0, 50)).thenReturn(List.of(kGroupOne));
when(keycloakClient.getClientRoles(clientOne.getId())).thenReturn(List.of(keycloakRoleB));

SecurityRequestBodyRepresentation securityRequestBodyRepresentation = new SecurityRequestBodyRepresentation();
Expand Down Expand Up @@ -618,8 +622,8 @@ void should_getApplicationPermissions_when_filteringByRole() {

@Test
void should_returnUsers_when_searchingByGroupName() {
when(keycloakClient.getUsersByGroupId(eq(groupOne.getId()))).thenReturn(List.of(kUserOne));
when(keycloakClient.searchGroups(eq(groupOne.getName()), eq(0), eq(50))).thenReturn(List.of(kGroupOne));
when(keycloakClient.getUsersByGroupId(groupOne.getId())).thenReturn(List.of(kUserOne));
when(keycloakClient.searchGroups(groupOne.getName(), 0, 50)).thenReturn(List.of(kGroupOne));

List<User> users = keycloakManagementService.findUsersByGroupName(groupOne.getName());

Expand Down Expand Up @@ -725,6 +729,34 @@ void should_throwExceptionIfGroupIsNotFound() {
.hasMessage("Invalid Security data: group {groupOne} is invalid or doesn't exist");
}

@Test
void should_searchByUsername_whenUserTypeSearchParamIsAll() {
defineSearchUsersByUsernameFromKeycloak();
setUpUsersRealmRoles();
UserSearchParams userSearchParams = new UserSearchParams();
userSearchParams.setType(UserTypeSearchParam.ALL);
String searchKey = "o";
userSearchParams.setSearch(searchKey);

keycloakManagementService.findUsers(userSearchParams);

verify(keycloakClient).searchUsersByUsername(searchKey);
}

@Test
void should_searchByKeyword_whenUserTypeSearchParamIsInteractive() {
defineSearchUsersFromKeycloak();
setUpUsersRealmRoles();
UserSearchParams userSearchParams = new UserSearchParams();
userSearchParams.setType(UserTypeSearchParam.INTERACTIVE);
String searchKey = "o";
userSearchParams.setSearch(searchKey);

keycloakManagementService.findUsers(userSearchParams);

verify(keycloakClient).searchUsers(searchKey, 0, 50);
}

private void assertThatGroupsAreEqual(List<Group> groups, Stream<Group> groupsToCompare) {
assertTrue(
groupsToCompare
Expand All @@ -739,20 +771,23 @@ private void assertThatGroupsAreEqual(List<Group> groups, Stream<Group> groupsTo
}

private void defineSearchGroupsFromKeycloak() {
when(keycloakClient.searchGroups(eq("o"), eq(0), eq(50)))
when(keycloakClient.searchGroups("o", 0, 50))
.thenReturn(List.of(kGroupOne, kGroupTwo, kGroupThree, kGroupFour));
}

private void defineSearchUsersFromKeycloak() {
when(keycloakClient.searchUsers(eq("o"), eq(0), eq(50)))
.thenReturn(List.of(kUserOne, kUserTwo, kUserThree, kUserFour));
when(keycloakClient.searchUsers("o", 0, 50)).thenReturn(List.of(kUserOne, kUserTwo, kUserThree, kUserFour));
}

private void defineSearchUsersByUsernameFromKeycloak() {
when(keycloakClient.searchUsersByUsername("o")).thenReturn(List.of(kUserOne, kUserTwo, kUserFour));
}

private void defineSearchUsersByGroupsFromKeycloak() {
when(keycloakClient.searchGroups(eq(groupOne.getName()), eq(0), eq(50))).thenReturn(List.of(kGroupOne));
when(keycloakClient.searchGroups(groupOne.getName(), 0, 50)).thenReturn(List.of(kGroupOne));
when(keycloakClient.getUsersByGroupId(kGroupOne.getId())).thenReturn(List.of(kUserOne, kUserTwo));

when(keycloakClient.searchGroups(eq(groupTwo.getName()), eq(0), eq(50))).thenReturn(List.of(kGroupTwo));
when(keycloakClient.searchGroups(groupTwo.getName(), 0, 50)).thenReturn(List.of(kGroupTwo));
when(keycloakClient.getUsersByGroupId(kGroupTwo.getId())).thenReturn(List.of(kUserTwo, kUserThree));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class UserSearchParams {
private String search;
private Set<String> groups;
private Set<String> roles;
private UserTypeSearchParam type;
private String application;

public String getSearchKey() {
Expand All @@ -48,6 +49,14 @@ public void setRoles(Set<String> roles) {
this.roles = roles;
}

public UserTypeSearchParam getType() {
return type;
}

public void setType(UserTypeSearchParam type) {
this.type = type;
}

public String getApplication() {
return application;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2017-2020 Alfresco Software, Ltd.
*
* 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.activiti.cloud.identity;

import java.util.Arrays;
import org.activiti.cloud.identity.exceptions.IdentityInvalidUserTypeException;

public enum UserTypeSearchParam {
ALL,
INTERACTIVE;

public static UserTypeSearchParam convertFromStringOrThrow(String stringValue) {
return Arrays
.stream(UserTypeSearchParam.values())
.filter(ut -> ut.name().equals(stringValue))
.findAny()
.orElseThrow(() -> new IdentityInvalidUserTypeException(stringValue));
}
}
Loading

0 comments on commit 6732338

Please sign in to comment.