TSK-1308: Retrieve users by DN without using userSearchBase.

This commit is contained in:
Holger Hagen 2021-07-05 16:05:47 +02:00 committed by holgerhagen
parent 047ce3e6cc
commit 95fd731d25
9 changed files with 244 additions and 86 deletions

View File

@ -16,6 +16,11 @@ cn: users
objectclass: top
objectclass: container
dn: cn=other-users,OU=Test,O=TASKANA
cn: users
objectclass: top
objectclass: container
dn: cn=organisation,OU=Test,O=TASKANA
cn: organisation
objectclass: top
@ -332,6 +337,22 @@ ou: Organisationseinheit/Organisationseinheit B
cn: Brunhilde Bio
userPassword: user-b-2
########################
# Users in other cn
########################
dn: uid=otheruser,cn=other-users,OU=Test,O=TASKANA
objectclass: inetorgperson
objectclass: organizationalperson
objectclass: person
objectclass: top
givenName: Other
description: User in other cn than search root
uid: otheruser
sn: User
ou: Other
cn: Other User
userPassword: otheruser
########################
# Groups

View File

@ -0,0 +1,34 @@
package pro.taskana.example.ldap;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import pro.taskana.common.rest.models.AccessIdRepresentationModel;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
/** Test Ldap attachment. */
@TaskanaSpringBootTest
@ActiveProfiles({"emptySearchRoots"})
class LdapEmptySearchRootsTest extends LdapTest {
@Test
void should_findGroupsForUser_When_UserIdIsProvided() throws Exception {
List<AccessIdRepresentationModel> groups =
ldapClient.searchGroupsAccessIdIsMemberOf("user-2-2");
assertThat(groups)
.extracting(AccessIdRepresentationModel::getAccessId)
.containsExactlyInAnyOrder(
"cn=ksc-users,cn=groups,ou=test,o=taskana",
"cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,"
+ "cn=organisation,ou=test,o=taskana");
}
@Test
void should_returnFullDnForUser_When_AccessIdOfUserIsGiven() {
String dn = ldapClient.searchDnForAccessId("otheruser");
assertThat(dn).isEqualTo("uid=otheruser,cn=other-users,ou=test,o=taskana");
}
}

View File

@ -10,11 +10,14 @@ import pro.taskana.common.rest.ldap.LdapClient;
import pro.taskana.common.rest.models.AccessIdRepresentationModel;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
/** Test Ldap attachment. */
/**
* Test Ldap attachment.
*/
@TaskanaSpringBootTest
class LdapTest {
@Autowired private LdapClient ldapClient;
@Autowired
LdapClient ldapClient;
@Test
void testFindUsers() throws Exception {
@ -22,7 +25,7 @@ class LdapTest {
assertThat(usersAndGroups)
.extracting(AccessIdRepresentationModel::getAccessId)
.containsExactlyInAnyOrder(
"teamlead-1", "teamlead-2", "cn=ksc-teamleads,cn=groups,ou=Test,O=TASKANA");
"teamlead-1", "teamlead-2", "cn=ksc-teamleads,cn=groups,ou=test,o=taskana");
}
@Test
@ -33,4 +36,21 @@ class LdapTest {
usersAndGroups = ldapClient.searchUsersAndGroups("Elena Faul");
assertThat(usersAndGroups).hasSize(1);
}
@Test
void should_findGroupsForUser_When_UserIdIsProvided() throws Exception {
List<AccessIdRepresentationModel> groups = ldapClient
.searchGroupsAccessIdIsMemberOf("user-2-2");
assertThat(groups)
.extracting(AccessIdRepresentationModel::getAccessId)
.containsExactlyInAnyOrder(
"cn=ksc-users,cn=groups,ou=test,o=taskana");
}
@Test
void should_returnFullDnForUser_When_AccessIdOfUserIsGiven() {
String dn = ldapClient.searchDnForAccessId("user-2-2");
assertThat(dn).isEqualTo("uid=user-2-2,cn=users,ou=test,o=taskana");
}
}

View File

@ -0,0 +1,47 @@
logging.level.pro.taskana=INFO
logging.level.org.springframework.security=INFO
logging.level.org.springframework.ldap=INFO
### logging.level.org.springframework=DEBUG
######## Taskana DB #######
datasource.url=jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0
datasource.driverClassName=org.h2.Driver
datasource.username=sa
datasource.password=sa
taskana.schemaName=TASKANA
####### property that control rest api security deploy use true for no security.
devMode=false
####### Properties for AccessIdController to connect to LDAP
taskana.ldap.serverUrl=ldap://localhost:11389
taskana.ldap.bindDn=uid=admin
taskana.ldap.bindPassword=secret
taskana.ldap.baseDn=ou=Test,O=TASKANA
taskana.ldap.userSearchBase=
taskana.ldap.userSearchFilterName=objectclass
taskana.ldap.userSearchFilterValue=person
taskana.ldap.userFirstnameAttribute=givenName
taskana.ldap.userLastnameAttribute=sn
taskana.ldap.userFullnameAttribute=cn
taskana.ldap.userIdAttribute=uid
taskana.ldap.userMemberOfGroupAttribute=memberOf
taskana.ldap.groupSearchBase=
taskana.ldap.groupSearchFilterName=objectclass
taskana.ldap.groupSearchFilterValue=groupOfUniqueNames
taskana.ldap.groupNameAttribute=cn
taskana.ldap.minSearchForLength=3
taskana.ldap.maxNumberOfReturnedAccessIds=50
taskana.ldap.groupsOfUser=uniquemember
# Embedded Spring LDAP server
spring.ldap.embedded.base-dn=OU=Test,O=TASKANA
spring.ldap.embedded.credential.username=uid=admin
spring.ldap.embedded.credential.password=secret
spring.ldap.embedded.ldif=classpath:taskana-test.ldif
spring.ldap.embedded.port=11389
spring.ldap.embedded.validation.enabled=false
####### JobScheduler cron expression that specifies when the JobSchedler runs
taskana.jobscheduler.async.cron=0 0 * * * *
####### cache static resources properties
spring.web.resources.cache.cachecontrol.cache-private=true
spring.main.allow-bean-definition-overriding=true
####### tomcat is not detecting the x-forward headers from bluemix as a trustworthy proxy
server.tomcat.remoteip.internal-proxies=.*
server.forward-headers-strategy=native

View File

@ -2,7 +2,6 @@ logging.level.pro.taskana=INFO
logging.level.org.springframework.security=INFO
logging.level.org.springframework.ldap=INFO
### logging.level.org.springframework=DEBUG
######## Taskana DB #######
datasource.url=jdbc:h2:mem:taskana;IGNORECASE=TRUE;LOCK_MODE=0
datasource.driverClassName=org.h2.Driver
@ -11,7 +10,6 @@ datasource.password=sa
taskana.schemaName=TASKANA
####### property that control rest api security deploy use true for no security.
devMode=false
####### Properties for AccessIdController to connect to LDAP
taskana.ldap.serverUrl=ldap://localhost:11389
taskana.ldap.bindDn=uid=admin
@ -31,14 +29,13 @@ taskana.ldap.groupSearchFilterValue=groupOfUniqueNames
taskana.ldap.groupNameAttribute=cn
taskana.ldap.minSearchForLength=3
taskana.ldap.maxNumberOfReturnedAccessIds=50
taskana.ldap.groupsOfUser=memberUid
taskana.ldap.groupsOfUser=uniquemember
# Embedded Spring LDAP server
spring.ldap.embedded.base-dn= OU=Test,O=TASKANA
spring.ldap.embedded.credential.username= uid=admin
spring.ldap.embedded.credential.password= secret
spring.ldap.embedded.base-dn=OU=Test,O=TASKANA
spring.ldap.embedded.credential.username=uid=admin
spring.ldap.embedded.credential.password=secret
spring.ldap.embedded.ldif=classpath:taskana-test.ldif
spring.ldap.embedded.port= 11389
spring.ldap.embedded.port=11389
spring.ldap.embedded.validation.enabled=false
####### JobScheduler cron expression that specifies when the JobSchedler runs
taskana.jobscheduler.async.cron=0 0 * * * *

View File

@ -33,12 +33,12 @@ public class AccessIdController {
/**
* This endpoint searches a provided access Id in the configured ldap.
*
* @title Search for Access Id (users and groups)
* @param searchFor the Access Id which should be searched for.
* @return a list of all found Access Ids
* @throws InvalidArgumentException if the provided search for Access Id is shorter than the
* configured one.
* @throws NotAuthorizedException if the current user is not ADMIN or BUSINESS_ADMIN.
* @title Search for Access Id (users and groups)
*/
@GetMapping(path = RestEndpoints.URL_ACCESS_ID)
public ResponseEntity<List<AccessIdRepresentationModel>> searchUsersAndGroups(
@ -56,7 +56,6 @@ public class AccessIdController {
* will only work if the users in the configured LDAP have an attribute that shows their group
* memberships, e.g. "memberOf"
*
* @title Search for Access Id (users) in TASKANA user role
* @param nameOrAccessId the name or Access Id which should be searched for.
* @param role the role for which all users should be searched for
* @return a list of all found Access Ids (users)
@ -64,6 +63,7 @@ public class AccessIdController {
* configured one.
* @throws NotAuthorizedException if the current user is not member of role USER, BUSINESS_ADMIN
* or ADMIN
* @title Search for Access Id (users) in TASKANA user role
*/
@GetMapping(path = RestEndpoints.URL_USER)
public ResponseEntity<List<AccessIdRepresentationModel>> searchUsersByNameOrAccessIdForRole(
@ -85,11 +85,11 @@ public class AccessIdController {
/**
* This endpoint retrieves all groups a given Access Id belongs to.
*
* @title Get groups for Access Id
* @param accessId the Access Id whose groups should be determined.
* @return a list of the group Access Ids the requested Access Id belongs to
* @throws InvalidArgumentException if the requested Access Id does not exist or is not unique.
* @throws NotAuthorizedException if the current user is not ADMIN or BUSINESS_ADMIN.
* @title Get groups for Access Id
*/
@GetMapping(path = RestEndpoints.URL_ACCESS_ID_GROUPS)
public ResponseEntity<List<AccessIdRepresentationModel>> getGroupsByAccessId(
@ -97,10 +97,6 @@ public class AccessIdController {
throws InvalidArgumentException, NotAuthorizedException {
taskanaEngine.checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.BUSINESS_ADMIN);
if (!ldapClient.validateAccessId(accessId)) {
throw new InvalidArgumentException("The accessId is invalid");
}
List<AccessIdRepresentationModel> accessIds =
ldapClient.searchGroupsAccessIdIsMemberOf(accessId);

View File

@ -28,6 +28,7 @@ import pro.taskana.TaskanaEngineConfiguration;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.api.exceptions.TaskanaRuntimeException;
import pro.taskana.common.rest.models.AccessIdRepresentationModel;
/** Class for Ldap access. */
@ -40,6 +41,7 @@ public class LdapClient {
private final TaskanaEngineConfiguration taskanaEngineConfiguration;
private final Environment env;
private final LdapTemplate ldapTemplate;
private final boolean useLowerCaseForAccessIds;
private boolean active = false;
private int minSearchForLength;
private int maxNumberOfReturnedAccessIds;
@ -53,6 +55,7 @@ public class LdapClient {
this.env = env;
this.ldapTemplate = ldapTemplate;
this.taskanaEngineConfiguration = taskanaEngineConfiguration;
this.useLowerCaseForAccessIds = TaskanaEngineConfiguration.shouldUseLowerCaseForAccessIds();
}
/**
@ -141,6 +144,8 @@ public class LdapClient {
orFilter.or(new WhitespaceWildcardsFilter(getUserIdAttribute(), name));
andFilter.and(orFilter);
LOGGER.debug("Using filter '{}' for LDAP query.", andFilter);
return ldapTemplate.search(
getUserSearchBase(),
andFilter.encode(),
@ -160,6 +165,8 @@ public class LdapClient {
getUserFirstnameAttribute(), getUserLastnameAttribute(), getUserIdAttribute()
};
LOGGER.debug("Using filter '{}' for LDAP query.", andFilter);
return ldapTemplate.search(
getUserSearchBase(),
andFilter.encode(),
@ -182,6 +189,8 @@ public class LdapClient {
}
andFilter.and(orFilter);
LOGGER.debug("Using filter '{}' for LDAP query.", andFilter);
return ldapTemplate.search(
getGroupSearchBase(),
andFilter.encode(),
@ -210,23 +219,25 @@ public class LdapClient {
isInitOrFail();
testMinSearchForLength(accessId);
String dn = searchDnForAccessId(accessId);
if (dn == null || dn.isEmpty()) {
throw new InvalidArgumentException("The AccessId is invalid");
}
final AndFilter andFilter = new AndFilter();
andFilter.and(new EqualsFilter(getGroupSearchFilterName(), getGroupSearchFilterValue()));
final OrFilter orFilter = new OrFilter();
orFilter.or(new EqualsFilter(getGroupsOfUser(), accessId));
orFilter.or(
new EqualsFilter(
getGroupsOfUser(),
LdapNameBuilder.newInstance()
.add(getBaseDn())
.add(getUserSearchBase())
.add(getUserIdAttribute(), accessId)
.build()
.toString()));
orFilter.or(new EqualsFilter(getGroupsOfUser(), dn));
andFilter.and(orFilter);
String[] userAttributesToReturn = {getUserIdAttribute(), getGroupNameAttribute()};
LOGGER.debug(
"Using filter '{}' for LDAP query with group search base {}.",
andFilter,
getGroupSearchBase());
return ldapTemplate.search(
getGroupSearchBase(),
andFilter.encode(),
@ -235,6 +246,53 @@ public class LdapClient {
new GroupContextMapper());
}
/**
* Performs a lookup to retrieve correct DN for the given accessId.
*
* @param accessId The AccessId to lookup
* @return the LDAP Distinguished Name for the AccessId
* @throws TaskanaRuntimeException thrown if the given AccessId is ambiguous.
*/
public String searchDnForAccessId(String accessId) throws TaskanaRuntimeException {
isInitOrFail();
if (nameIsDn(accessId)) {
AccessIdRepresentationModel groupByDn = searchAccessIdByDn(accessId);
return groupByDn.getAccessId();
} else {
final AndFilter andFilter = new AndFilter();
andFilter.and(new EqualsFilter(getUserSearchFilterName(), getUserSearchFilterValue()));
final OrFilter orFilter = new OrFilter();
orFilter.or(new EqualsFilter(getUserIdAttribute(), accessId));
andFilter.and(orFilter);
LOGGER.debug(
"Using filter '{}' for LDAP query with user search base {}.",
andFilter,
getUserSearchBase());
final List<String> distinguishedNames =
ldapTemplate.search(
getUserSearchBase(),
andFilter.encode(),
SearchControls.SUBTREE_SCOPE,
null,
new AbstractContextMapper<String>() {
public String doMapFromContext(DirContextOperations ctx) {
return getDnFromContext(ctx);
}
});
if (distinguishedNames == null || distinguishedNames.isEmpty()) {
return null;
} else if (distinguishedNames.size() > 1) {
throw new TaskanaRuntimeException("Ambiguous AccessId found: " + accessId);
} else {
return distinguishedNames.get(0);
}
}
}
/**
* Validates a given AccessId / name.
*
@ -444,26 +502,26 @@ public class LdapClient {
}
}
String getDnWithBaseDn(final String givenDn) {
String dn = givenDn;
if (!dn.toLowerCase().endsWith(getBaseDn().toLowerCase())) {
dn = dn + "," + getBaseDn();
}
return dn;
}
private String getUserFullnameAttribute() {
return LdapSettings.TASKANA_LDAP_USER_FULLNAME_ATTRIBUTE.getValueFromEnv(env);
}
private String getDnFromContext(final DirContextOperations context) {
String dn = LdapNameBuilder.newInstance(getBaseDn()).add(context.getDn()).build().toString();
if (useLowerCaseForAccessIds) {
return dn.toLowerCase();
} else {
return dn;
}
}
/** Context Mapper for user entries. */
class GroupContextMapper extends AbstractContextMapper<AccessIdRepresentationModel> {
@Override
public AccessIdRepresentationModel doMapFromContext(final DirContextOperations context) {
final AccessIdRepresentationModel accessId = new AccessIdRepresentationModel();
String dn = getDnWithBaseDn(context.getDn().toString());
accessId.setAccessId(dn); // fully qualified dn
accessId.setAccessId(getDnFromContext(context)); // fully qualified dn
accessId.setName(context.getStringAttribute(getGroupNameAttribute()));
return accessId;
}
@ -496,8 +554,7 @@ public class LdapClient {
String lastName = context.getStringAttribute(getUserLastnameAttribute());
accessId.setName(String.format("%s, %s", lastName, firstName));
} else {
String dn = getDnWithBaseDn(context.getDn().toString());
accessId.setAccessId(dn); // fully qualified dn
accessId.setAccessId(getDnFromContext(context)); // fully qualified dn
accessId.setName(context.getStringAttribute(getGroupNameAttribute()));
}
return accessId;

View File

@ -168,14 +168,11 @@ class AccessIdControllerIntTest {
restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + "?access-id=teamlead-2,cn=users";
HttpEntity<Object> auth = new HttpEntity<>(restHelper.getHeadersTeamlead_1());
ThrowingCallable call =
() -> {
TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE);
};
ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE);
assertThatThrownBy(call)
.isInstanceOf(HttpClientErrorException.class)
.hasMessageContaining("The accessId is invalid")
.hasMessageContaining("The AccessId is invalid")
.extracting(ex -> ((HttpClientErrorException) ex).getStatusCode())
.isEqualTo(HttpStatus.BAD_REQUEST);
}
@ -203,10 +200,7 @@ class AccessIdControllerIntTest {
String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + "?access-id=teamlead-2";
HttpEntity<Object> auth = new HttpEntity<>(restHelper.getHeadersUser_1_1());
ThrowingCallable call =
() -> {
TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE);
};
ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE);
assertThatThrownBy(call)
.isInstanceOf(HttpClientErrorException.class)
@ -219,10 +213,7 @@ class AccessIdControllerIntTest {
String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=al";
HttpEntity<Object> auth = new HttpEntity<>(restHelper.getHeadersUser_1_1());
ThrowingCallable call =
() -> {
TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE);
};
ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE);
assertThatThrownBy(call)
.isInstanceOf(HttpClientErrorException.class)

View File

@ -35,13 +35,17 @@ import pro.taskana.common.rest.models.AccessIdRepresentationModel;
@ExtendWith(MockitoExtension.class)
class LdapClientTest {
@Mock Environment environment;
@Mock
Environment environment;
@Mock LdapTemplate ldapTemplate;
@Mock
LdapTemplate ldapTemplate;
@Mock TaskanaEngineConfiguration taskanaEngineConfiguration;
@Mock
TaskanaEngineConfiguration taskanaEngineConfiguration;
@InjectMocks LdapClient cut;
@InjectMocks
LdapClient cut;
@Test
void testLdap_searchGroupByDn() {
@ -65,10 +69,10 @@ class LdapClientTest {
AccessIdRepresentationModel user = new AccessIdRepresentationModel("testU", "testUId");
when(ldapTemplate.search(
any(String.class), any(), anyInt(), any(), any(LdapClient.GroupContextMapper.class)))
any(String.class), any(), anyInt(), any(), any(LdapClient.GroupContextMapper.class)))
.thenReturn(List.of(group));
when(ldapTemplate.search(
any(String.class), any(), anyInt(), any(), any(LdapClient.UserContextMapper.class)))
any(String.class), any(), anyInt(), any(), any(LdapClient.UserContextMapper.class)))
.thenReturn(List.of(user));
assertThat(cut.searchUsersAndGroups("test")).hasSize(2).containsExactlyInAnyOrder(user, group);
@ -107,7 +111,7 @@ class LdapClientTest {
when(taskanaEngineConfiguration.getRoleMap()).thenReturn(roleMap);
when(ldapTemplate.search(
any(String.class), any(), anyInt(), any(), any(LdapClient.UserContextMapper.class)))
any(String.class), any(), anyInt(), any(), any(LdapClient.UserContextMapper.class)))
.thenReturn(List.of(user));
assertThat(cut.searchUsersByNameOrAccessIdInUserRole("test")).hasSize(1).containsExactly(user);
@ -158,36 +162,27 @@ class LdapClientTest {
assertThat(cut.nameIsDn("uid=userid,cn=users,o=taskana")).isFalse();
}
@Test
void testDnIsCompletedCorrectly() {
setUpEnvMock();
assertThat(cut.getDnWithBaseDn("uid=userid,cn=users,o=TaskanaTest"))
.isEqualTo("uid=userid,cn=users,o=TaskanaTest");
assertThat(cut.getDnWithBaseDn("uid=userid,cn=users"))
.isEqualTo("uid=userid,cn=users,o=TaskanaTest");
}
private void setUpEnvMock() {
Stream.of(
new String[][] {
{"taskana.ldap.minSearchForLength", "3"},
{"taskana.ldap.maxNumberOfReturnedAccessIds", "50"},
{"taskana.ldap.baseDn", "o=TaskanaTest"},
{"taskana.ldap.userSearchBase", "ou=people"},
{"taskana.ldap.userSearchFilterName", "objectclass"},
{"taskana.ldap.groupsOfUser", "memberUid"},
{"taskana.ldap.groupNameAttribute", "cn"},
{"taskana.ldap.groupSearchFilterValue", "groupOfUniqueNames"},
{"taskana.ldap.groupSearchFilterName", "objectclass"},
{"taskana.ldap.groupSearchBase", "ou=groups"},
{"taskana.ldap.userIdAttribute", "uid"},
{"taskana.ldap.userMemberOfGroupAttribute", "memberOf"},
{"taskana.ldap.userLastnameAttribute", "sn"},
{"taskana.ldap.userFirstnameAttribute", "givenName"},
{"taskana.ldap.userFullnameAttribute", "cn"},
{"taskana.ldap.userSearchFilterValue", "person"}
})
new String[][]{
{"taskana.ldap.minSearchForLength", "3"},
{"taskana.ldap.maxNumberOfReturnedAccessIds", "50"},
{"taskana.ldap.baseDn", "o=TaskanaTest"},
{"taskana.ldap.userSearchBase", "ou=people"},
{"taskana.ldap.userSearchFilterName", "objectclass"},
{"taskana.ldap.groupsOfUser", "memberUid"},
{"taskana.ldap.groupNameAttribute", "cn"},
{"taskana.ldap.groupSearchFilterValue", "groupOfUniqueNames"},
{"taskana.ldap.groupSearchFilterName", "objectclass"},
{"taskana.ldap.groupSearchBase", "ou=groups"},
{"taskana.ldap.userIdAttribute", "uid"},
{"taskana.ldap.userMemberOfGroupAttribute", "memberOf"},
{"taskana.ldap.userLastnameAttribute", "sn"},
{"taskana.ldap.userFirstnameAttribute", "givenName"},
{"taskana.ldap.userFullnameAttribute", "cn"},
{"taskana.ldap.userSearchFilterValue", "person"}
})
.forEach(
strings ->
lenient().when(this.environment.getProperty(strings[0])).thenReturn(strings[1]));