From 31100a54ddbb67839a8b71a091d11069f18aa44e Mon Sep 17 00:00:00 2001 From: MM1277 Date: Tue, 9 Apr 2024 14:53:02 +0200 Subject: [PATCH] Closes #2563 - add handling of permissions as access ids Co-authored-by: SebastianRoseneck <55637012+SebastianRoseneck@users.noreply.github.com> Co-authored-by: ryzheboka <025465835+ryzheboka@users.noreply.github.com> --- .../sample-data/workbasket-access-list.sql | 9 +- .../sql/test-data/workbasket-access-list.sql | 5 + .../common/test/security/WithAccessId.java | 2 + .../test/security/JaasExtensionTest.java | 22 +- .../src/test/resources/application.properties | 6 + .../test/java/acceptance/AbstractAccTest.java | 2 + .../QueryWorkbasketAccessItemsAccTest.java | 23 +- .../QueryWorkbasketByPermissionAccTest.java | 58 +-- .../query/WorkbasketQueryAccTest.java | 12 +- .../testapi/security/WithAccessId.java | 2 + .../security/BootWebSecurityConfigurer.java | 7 + .../src/main/resources/application.properties | 6 + .../controllers/taskana-customization.json | 3 + .../src/main/resources/taskana-example.ldif | 44 ++- ...rchRootsForUseDnForGroupsDisabledTest.java | 51 +++ .../ldap/LdapEmptySearchRootsTest.java | 16 + .../LdapForUseDnForGroupsDisabledTest.java | 110 ++++++ .../pro/taskana/example/ldap/LdapTest.java | 30 +- .../application-emptySearchRoots.properties | 1 + .../src/test/resources/application.properties | 11 +- .../src/main/resources/application.properties | 6 + .../TaskanaWildflyWithUserConfigTest.java | 2 +- ...ion-with-additional-user-config.properties | 7 + .../src/test/resources/application.properties | 6 + .../src/test/resources/taskana-test.ldif | 22 ++ .../rest/test/TestWebSecurityConfig.java | 5 +- .../src/main/resources/taskana-test.ldif | 44 ++- .../common/rest/AccessIdController.java | 36 +- .../taskana/common/rest/RestEndpoints.java | 1 + .../taskana/common/rest/ldap/LdapClient.java | 347 ++++++++++++++++-- .../common/rest/ldap/LdapSettings.java | 10 +- .../TaskanaUserInfoRepresentationModel.java | 2 +- ...ollerForUseDnForGroupsDisabledIntTest.java | 280 ++++++++++++++ .../rest/AccessIdControllerIntTest.java | 54 ++- .../rest/TaskanaEngineControllerIntTest.java | 44 +++ .../common/rest/ldap/LdapClientTest.java | 99 +++-- .../rest/WorkbasketControllerIntTest.java | 5 +- .../src/test/resources/application.properties | 7 + .../src/test/resources/application.properties | 6 + .../access-items-management.component.html | 23 ++ .../access-items-management.component.scss | 10 + .../access-items-management.component.spec.ts | 34 ++ .../access-items-management.component.ts | 95 ++++- .../workbasket-access-items.component.ts | 8 +- .../type-ahead/type-ahead.component.spec.ts | 24 +- .../type-ahead/type-ahead.component.ts | 39 +- web/src/app/shared/models/customisation.ts | 5 + .../services/access-ids/access-ids.service.ts | 7 + .../forms-validator.service.ts | 11 +- .../obtain-message/message-by-error-code.ts | 6 + .../access-items-management.actions.ts | 5 + .../access-items-management.selector.ts | 5 + .../access-items-management.state.ts | 22 ++ .../engine-configuration.selectors.ts | 8 +- .../app/shared/store/mock-data/mock-store.ts | 3 + .../data-sources/taskana-customization.json | 3 + 56 files changed, 1496 insertions(+), 215 deletions(-) create mode 100644 rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsForUseDnForGroupsDisabledTest.java create mode 100644 rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapForUseDnForGroupsDisabledTest.java create mode 100644 rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerForUseDnForGroupsDisabledIntTest.java diff --git a/common/taskana-common-data/src/main/resources/sql/sample-data/workbasket-access-list.sql b/common/taskana-common-data/src/main/resources/sql/sample-data/workbasket-access-list.sql index 3c173fdbae..4f32521195 100644 --- a/common/taskana-common-data/src/main/resources/sql/sample-data/workbasket-access-list.sql +++ b/common/taskana-common-data/src/main/resources/sql/sample-data/workbasket-access-list.sql @@ -1,7 +1,6 @@ -- sample-data is used for rest tests and for the example application ---SERT INTO WORKBASKET_ACCESS_LIST VALUES (ID , WB_ID , ACCESS_ID , ACCESS_NAME , READ , OPEN , APPEND, TRANSFER, DISTRIBUTE, C1 , C2 , C3 , C4 , C5 , C6 , C7 , C8 , C9 , C10 , C11 , C12 , READTASKS, EDITTASKS) --- KSC authorizations +-- KSC authorizations (ID , WB_ID , ACCESS_ID , ACCESS_NAME , READ , OPEN , APPEND, TRANSFER, DISTRIBUTE, C1, .., C12) -- PPKs INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000001', 'WBI:100000000000000000000000000000000004', 'teamlead-1' , 'Titus Toll' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000002', 'WBI:100000000000000000000000000000000005', 'teamlead-2' , 'Frauke Faul' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); @@ -37,7 +36,6 @@ INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:10000000000000000000000000000000 INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000017', 'WBI:100000000000000000000000000000000008', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , false, false , false , false , false , false , false , false , false , false , false , false , false , false , false , false ,true , true); INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000018', 'WBI:100000000000000000000000000000000009', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , false, true , false , false , false , false , false , false , false , false , false , false , false , false , false , false ,true , true); ---SERT INTO WORKBASKET_ACCESS_LIST VALUES (ID , WB_ID , ACCESS_ID , ACCESS_NAME , READ , OPEN , APPEND, TRANSFER, DISTRIBUTE, C1 , C2 , C3 , C4 , C5 , C6 , C7 , C8 , C9 , C10 , C11 , C12 ,READTASKS, EDITTASKS) -- Team GPK access INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000019', 'WBI:100000000000000000000000000000000002', 'cn=organisationseinheit ksc 1,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 1', true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:100000000000000000000000000000000020', 'WBI:100000000000000000000000000000000003', 'cn=organisationseinheit ksc 2,cn=organisationseinheit ksc,cn=organisation,ou=test,o=taskana', 'Organisationseinheit KSC 2', true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); @@ -64,3 +62,8 @@ INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:00000000000000000000000000000000 INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000907', 'WBI:000000000000000000000000000000000907', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000908', 'WBI:000000000000000000000000000000000908', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000909', 'WBI:000000000000000000000000000000000909', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); + +-- permissions +INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:200000000000000000000000000000000002', 'WBI:100000000000000000000000000000000005', 'taskana:callcenter:ab:ab/a:callcenter' , 'PERM_1' , true , true , true , false , true , false, false, false, false, false, false, false, false, false, false, false, false, true , false); +INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:200000000000000000000000000000000003', 'WBI:100000000000000000000000000000000006', 'taskana:callcenter:ab:AB/a:callcenter' , 'PERM_1' , true , false, true , true , false , false, false, false, false, false, false, false, false, false, false, false, false, true , true ); +INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:200000000000000000000000000000000005', 'WBI:100000000000000000000000000000000012', 'taskana:callcenter:ab:AB/a:callcenter' , 'PERM_1' , true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false, false , false); diff --git a/common/taskana-common-data/src/main/resources/sql/test-data/workbasket-access-list.sql b/common/taskana-common-data/src/main/resources/sql/test-data/workbasket-access-list.sql index 6a3f8edf91..58bd1dc646 100644 --- a/common/taskana-common-data/src/main/resources/sql/test-data/workbasket-access-list.sql +++ b/common/taskana-common-data/src/main/resources/sql/test-data/workbasket-access-list.sql @@ -51,3 +51,8 @@ INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:00000000000000000000000000000000 INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000907', 'WBI:000000000000000000000000000000000907', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000908', 'WBI:000000000000000000000000000000000908', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WBI:000000000000000000000000000000000909', 'WBI:000000000000000000000000000000000909', 'user-b-1' , 'Bern, Bernd' , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true , true); + +-- permissions +INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:200000000000000000000000000000000002', 'WBI:100000000000000000000000000000000005', 'taskana:callcenter:ab:ab/a:callcenter' , 'PERM_1' , true , true , true , false , true , false, false, false, false, false, false, false, false, false, false, false, false, true , false); +INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:200000000000000000000000000000000003', 'WBI:100000000000000000000000000000000006', 'taskana:callcenter:ab:ab/a:callcenter' , 'PERM_1' , true , false, true , true , false , false, false, false, false, false, false, false, false, false, false, false, false, true , true ); +INSERT INTO WORKBASKET_ACCESS_LIST VALUES ('WAI:200000000000000000000000000000000005', 'WBI:100000000000000000000000000000000012', 'taskana:callcenter:ab:ab/a:callcenter' , 'PERM_1' , true , false, true , false , false , false, false, false, false, false, false, false, false, false, false, false, false, false , false); diff --git a/common/taskana-common-test/src/main/java/pro/taskana/common/test/security/WithAccessId.java b/common/taskana-common-test/src/main/java/pro/taskana/common/test/security/WithAccessId.java index 9ac620b5f2..f496360876 100644 --- a/common/taskana-common-test/src/main/java/pro/taskana/common/test/security/WithAccessId.java +++ b/common/taskana-common-test/src/main/java/pro/taskana/common/test/security/WithAccessId.java @@ -16,6 +16,8 @@ String[] groups() default {}; + String[] permissions() default {}; + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface WithAccessIds { diff --git a/common/taskana-common-test/src/test/java/pro/taskana/common/test/security/JaasExtensionTest.java b/common/taskana-common-test/src/test/java/pro/taskana/common/test/security/JaasExtensionTest.java index fdfe2bec70..011bdc534a 100644 --- a/common/taskana-common-test/src/test/java/pro/taskana/common/test/security/JaasExtensionTest.java +++ b/common/taskana-common-test/src/test/java/pro/taskana/common/test/security/JaasExtensionTest.java @@ -7,7 +7,6 @@ import java.util.Iterator; import java.util.List; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -215,11 +214,12 @@ void should_SetMultipleJaasSubjects_When_MultipleAnnotationsExist_On_TestTemplat assertThat(CURRENT_USER_CONTEXT.getUserid()).isEqualTo(accessId.user()); } - @WithAccessId(user = "testtemplate1", groups = "abc") + @WithAccessId(user = "testtemplate1", groups = "abc", permissions = "perm") @TestTemplate void should_InjectCorrectAccessId_When_AnnotationExists_On_TestTemplate(WithAccessId accessId) { assertThat(accessId.user()).isEqualTo("testtemplate1"); assertThat(accessId.groups()).containsExactly("abc"); + assertThat(accessId.permissions()).containsExactly("perm"); } // endregion @@ -406,21 +406,21 @@ Stream should_SetAccessIdForDynamicContainerInStream_When_Anno @TestFactory Iterable should_NotSetAccessIdForDynamicTestInIterable_When_AnnotationIsMissing() { - return Stream.of(NULL_DYNAMIC_TEST, NULL_DYNAMIC_TEST).collect(Collectors.toList()); + return Stream.of(NULL_DYNAMIC_TEST, NULL_DYNAMIC_TEST).toList(); } @WithAccessId(user = INSIDE_DYNAMIC_TEST_USER) @TestFactory Iterable should_SetAccessIdForDynamicTestInIterable_When_AnnotationExists() { return Stream.of(DYNAMIC_TEST_USER_DYNAMIC_TEST, DYNAMIC_TEST_USER_DYNAMIC_TEST) - .collect(Collectors.toList()); + .toList(); } @WithAccessId(user = INSIDE_DYNAMIC_TEST_USER) @WithAccessId(user = INSIDE_DYNAMIC_TEST_USER) @TestFactory Iterable should_SetMultipleAccessIdForDynamicTestInIterable_When_AnnotationsExist() { - return Stream.of(NOT_NULL_DYNAMIC_TEST, NOT_NULL_DYNAMIC_TEST).collect(Collectors.toList()); + return Stream.of(NOT_NULL_DYNAMIC_TEST, NOT_NULL_DYNAMIC_TEST).toList(); } // WITH DynamicContainer @@ -431,7 +431,7 @@ Iterable should_SetMultipleAccessIdForDynamicTestInIterable_When_An Supplier supplier = () -> dynamicContainer("dynamic container", Stream.of(NULL_DYNAMIC_TEST, NULL_DYNAMIC_TEST)); - return Stream.generate(supplier).limit(2).collect(Collectors.toList()); + return Stream.generate(supplier).limit(2).toList(); } @WithAccessId(user = INSIDE_DYNAMIC_TEST_USER) @@ -443,7 +443,7 @@ Iterable should_SetMultipleAccessIdForDynamicTestInIterable_When_An dynamicContainer( "dynamic container", Stream.of(DYNAMIC_TEST_USER_DYNAMIC_TEST, DYNAMIC_TEST_USER_DYNAMIC_TEST)); - return Stream.generate(supplier).limit(2).collect(Collectors.toList()); + return Stream.generate(supplier).limit(2).toList(); } @WithAccessId(user = INSIDE_DYNAMIC_TEST_USER) @@ -455,7 +455,7 @@ Iterable should_SetMultipleAccessIdForDynamicTestInIterable_When_An () -> dynamicContainer( "dynamic container", Stream.of(NOT_NULL_DYNAMIC_TEST, NOT_NULL_DYNAMIC_TEST)); - return Stream.generate(supplier).limit(2).collect(Collectors.toList()); + return Stream.generate(supplier).limit(2).toList(); } // WITH nested DynamicContainer @@ -467,7 +467,7 @@ Iterable should_SetMultipleAccessIdForDynamicTestInIterable_When_An () -> dynamicContainer("inside container", Stream.of(NULL_DYNAMIC_TEST, NULL_DYNAMIC_TEST)); Supplier outsideSupplier = () -> dynamicContainer("outside container", Stream.of(supplier.get(), NULL_DYNAMIC_TEST)); - return Stream.generate(outsideSupplier).limit(2).collect(Collectors.toList()); + return Stream.generate(outsideSupplier).limit(2).toList(); } @WithAccessId(user = INSIDE_DYNAMIC_TEST_USER) @@ -483,7 +483,7 @@ Iterable should_SetMultipleAccessIdForDynamicTestInIterable_When_An () -> dynamicContainer( "outside container", Stream.of(supplier.get(), DYNAMIC_TEST_USER_DYNAMIC_TEST)); - return Stream.generate(outsideSupplier).limit(2).collect(Collectors.toList()); + return Stream.generate(outsideSupplier).limit(2).toList(); } @WithAccessId(user = INSIDE_DYNAMIC_TEST_USER) @@ -498,7 +498,7 @@ Iterable should_SetMultipleAccessIdForDynamicTestInIterable_When_An Supplier outsideSupplier = () -> dynamicContainer("outside container", Stream.of(supplier.get(), NOT_NULL_DYNAMIC_TEST)); - return Stream.generate(outsideSupplier).limit(2).collect(Collectors.toList()); + return Stream.generate(outsideSupplier).limit(2).toList(); } // endregion diff --git a/history/taskana-simplehistory-rest-spring/src/test/resources/application.properties b/history/taskana-simplehistory-rest-spring/src/test/resources/application.properties index 7866939c93..4032c0e2e5 100644 --- a/history/taskana-simplehistory-rest-spring/src/test/resources/application.properties +++ b/history/taskana-simplehistory-rest-spring/src/test/resources/application.properties @@ -34,6 +34,12 @@ taskana.ldap.groupNameAttribute=cn taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 taskana.ldap.groupsOfUser=memberUid +taskana.ldap.permissionSearchBase=cn=groups +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true # Embedded Spring LDAP server spring.ldap.embedded.base-dn=OU=Test,O=TASKANA spring.ldap.embedded.credential.username=uid=admin diff --git a/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java b/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java index 11908a091e..260a9737e7 100644 --- a/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java @@ -39,6 +39,8 @@ public abstract class AbstractAccTest { "cn=Organisationseinheit KSC 1,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA"; public static final String GROUP_2_DN = "cn=Organisationseinheit KSC 2,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA"; + public static final String PERM_1 = + "taskana:callcenter:ab:ab/a:callcenter"; protected static TaskanaConfiguration taskanaConfiguration; protected static TaskanaEngine taskanaEngine; diff --git a/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketAccessItemsAccTest.java b/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketAccessItemsAccTest.java index 069ab09a44..5744e48ed5 100644 --- a/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketAccessItemsAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketAccessItemsAccTest.java @@ -34,7 +34,7 @@ void testQueryWorkbasketAccessItemValuesForColumnName() throws Exception { columnValueList = workbasketService.createWorkbasketAccessItemQuery().listValues(ACCESS_ID, null); - assertThat(columnValueList).hasSize(10); + assertThat(columnValueList).hasSize(11); columnValueList = workbasketService.createWorkbasketAccessItemQuery().listValues(WORKBASKET_KEY, null); @@ -51,9 +51,9 @@ void testQueryAccessItemsForAccessIds() throws Exception { List results = workbasketService .createWorkbasketAccessItemQuery() - .accessIdIn("user-1-1", GROUP_1_DN) + .accessIdIn("user-1-1", GROUP_1_DN, PERM_1) .list(); - assertThat(results).hasSize(8); + assertThat(results).hasSize(11); } @WithAccessId(user = "unknownuser") @@ -78,12 +78,12 @@ void testQueryAccessItemsForAccessIdsOrderedDescending() throws Exception { WorkbasketAccessItemQuery query = workbasketService .createWorkbasketAccessItemQuery() - .accessIdIn("user-1-1", GROUP_1_DN) + .accessIdIn("user-1-1", GROUP_1_DN, PERM_1) .orderByAccessId(SortDirection.DESCENDING) .orderByWorkbasketId(SortDirection.DESCENDING); List results = query.list(); long count = query.count(); - assertThat(results).hasSize(8).size().isEqualTo(count); + assertThat(results).hasSize(11).size().isEqualTo(count); assertThat(results.get(0).getId()).isEqualTo("WAI:100000000000000000000000000000000003"); } @@ -94,12 +94,13 @@ void testQueryAccessItemsForAccessIdsAndWorkbasketKey() throws Exception { List results = workbasketService .createWorkbasketAccessItemQuery() - .accessIdIn("user-1-1", GROUP_1_DN) + .accessIdIn("user-1-1", GROUP_1_DN, PERM_1) .workbasketIdIn( "WBI:100000000000000000000000000000000006", - "WBI:100000000000000000000000000000000002") + "WBI:100000000000000000000000000000000002", + "WBI:100000000000000000000000000000000005") .list(); - assertThat(results).hasSize(3); + assertThat(results).hasSize(5); } @WithAccessId(user = "businessadmin") @@ -135,7 +136,7 @@ void testQueryAccessItemsByWorkbasketKey() throws Exception { .createWorkbasketAccessItemQuery() .workbasketIdIn("WBI:100000000000000000000000000000000006") .list(); - assertThat(results).hasSize(3); + assertThat(results).hasSize(4); } @WithAccessId(user = "businessadmin") @@ -149,7 +150,7 @@ void testQueryAccessItemsByWorkbasketKeyOrderedDescending() throws Exception { .orderByWorkbasketId(SortDirection.DESCENDING) .orderByAccessId(SortDirection.ASCENDING) .list(); - assertThat(results).hasSize(3); + assertThat(results).hasSize(4); assertThat(results.get(0).getId()).isEqualTo("WAI:100000000000000000000000000000000009"); } @@ -160,7 +161,7 @@ void testQueryForIdIn() throws Exception { String[] expectedIds = { "WAI:100000000000000000000000000000000001", "WAI:100000000000000000000000000000000015", - "WAI:100000000000000000000000000000000007" + "WAI:100000000000000000000000000000000006" }; List results = workbasketService.createWorkbasketAccessItemQuery().idIn(expectedIds).list(); diff --git a/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketByPermissionAccTest.java b/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketByPermissionAccTest.java index 70cd2311b7..cd982ec570 100644 --- a/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketByPermissionAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/workbasket/query/QueryWorkbasketByPermissionAccTest.java @@ -70,20 +70,21 @@ void should_ThrowNotAuthorizedException_When_QueryingWithUnknownUser() { @WithAccessId(user = "businessadmin") @Test - void should_GetAllTransferTargetsForUserAndGroup_When_QueryingForSinglePermission() + void should_GetAllTransferTargetsForUserGroupPermission_When_QueryingForSinglePermission() throws Exception { List results = WORKBASKET_SERVICE .createWorkbasketQuery() - .accessIdsHavePermissions(List.of(WorkbasketPermission.APPEND), "user-1-1", GROUP_1_DN) + .accessIdsHavePermissions(List.of(WorkbasketPermission.APPEND), "user-1-1", + GROUP_1_DN, PERM_1) .list(); - assertThat(results).hasSize(6); + assertThat(results).hasSize(7); } @WithAccessId(user = "businessadmin") @Test - void should_GetAllTransferTargetsForUserAndGroup_When_QueryingForMultiplePermissions() + void should_GetAllTransferTargetsForUserGroupPermission_When_QueryingForMultiplePermissions() throws Exception { List results = WORKBASKET_SERVICE @@ -91,35 +92,37 @@ void should_GetAllTransferTargetsForUserAndGroup_When_QueryingForMultiplePermiss .accessIdsHavePermissions( List.of(WorkbasketPermission.APPEND, WorkbasketPermission.OPEN), "user-1-1", - GROUP_1_DN) + GROUP_1_DN, PERM_1) .list(); - assertThat(results).hasSize(4); + assertThat(results).hasSize(5); } @WithAccessId(user = "businessadmin") @Test - void should_GetAllWorkbasketsForUserAndGroup_When_QueryingForReadTasksPermissions() + void should_GetAllWorkbasketsForUserGroupPermission_When_QueryingForReadTasksPermissions() throws Exception { List results = WORKBASKET_SERVICE .createWorkbasketQuery() .accessIdsHavePermissions( - List.of(WorkbasketPermission.READTASKS), "user-1-1", GROUP_1_DN) + List.of(WorkbasketPermission.READTASKS), "user-1-1", + GROUP_1_DN, PERM_1) .list(); - assertThat(results).hasSize(7); + assertThat(results).hasSize(8); } @WithAccessId(user = "businessadmin") @Test - void should_GetAllWorkbasketsForUserAndGroup_When_QueryingForEditTasksPermissions() + void should_GetAllWorkbasketsForUserGroupPermission_When_QueryingForEditTasksPermissions() throws Exception { List results = WORKBASKET_SERVICE .createWorkbasketQuery() .accessIdsHavePermissions( - List.of(WorkbasketPermission.READTASKS), "user-1-1", GROUP_1_DN) + List.of(WorkbasketPermission.EDITTASKS), "user-1-1", + GROUP_1_DN, PERM_1) .list(); assertThat(results).hasSize(7); @@ -127,49 +130,52 @@ void should_GetAllWorkbasketsForUserAndGroup_When_QueryingForEditTasksPermission @WithAccessId(user = "businessadmin") @Test - void should_GetAllTransferTargetsForUserAndGroup_When_QueryingForSortedByNameAscending() + void should_GetAllTransferTargetsForUserGroupPermission_When_QueryingForSortedByNameAscending() throws Exception { List results = WORKBASKET_SERVICE .createWorkbasketQuery() - .accessIdsHavePermissions(List.of(WorkbasketPermission.APPEND), "user-1-1", GROUP_1_DN) + .accessIdsHavePermissions(List.of(WorkbasketPermission.APPEND), + "user-1-1", GROUP_1_DN, PERM_1) .orderByName(SortDirection.ASCENDING) .list(); - assertThat(results).hasSize(6); + assertThat(results).hasSize(7); assertThat(results.get(0).getKey()).isEqualTo("GPK_KSC_1"); } @WithAccessId(user = "businessadmin") @Test - void should_GetAllTransferTargetsForUserAndGroup_When_QueryingForSortedByNameDescending() + void should_GetAllTransferTargetsForUserGroupPermission_When_QueryingForSortedByNameDescending() throws Exception { List results = WORKBASKET_SERVICE .createWorkbasketQuery() - .accessIdsHavePermissions(List.of(WorkbasketPermission.APPEND), "user-1-1", GROUP_1_DN) + .accessIdsHavePermissions(List.of(WorkbasketPermission.APPEND), + "user-1-1", GROUP_1_DN, PERM_1) .orderByName(SortDirection.DESCENDING) .orderByKey(SortDirection.ASCENDING) .list(); - assertThat(results).hasSize(6); + assertThat(results).hasSize(7); assertThat(results.get(0).getKey()).isEqualTo("USER-2-2"); } @WithAccessId(user = "businessadmin") @Test - void should_GetAllTransferSourcesForUserAndGroup_When_QueryingForSinglePermission() + void should_GetAllTransferSourcesForUserGroupPermission_When_QueryingForSinglePermission() throws Exception { List results = WORKBASKET_SERVICE .createWorkbasketQuery() .accessIdsHavePermissions( - List.of(WorkbasketPermission.DISTRIBUTE), "user-1-1", GROUP_1_DN) + List.of(WorkbasketPermission.DISTRIBUTE), + "user-1-1", GROUP_1_DN, PERM_1) .list(); assertThat(results) .extracting(WorkbasketSummary::getKey) - .containsExactlyInAnyOrder("GPK_KSC_1", "USER-1-1"); + .containsExactlyInAnyOrder("GPK_KSC_1", "USER-1-1", "TEAMLEAD-2"); } // endregion @@ -224,28 +230,28 @@ void should_GetAllTransferTargetsForSubjectUser_When_QueryingForMultiplePermissi assertThat(results).hasSize(3); } - @WithAccessId(user = "user-1-1", groups = GROUP_1_DN) + @WithAccessId(user = "user-1-1", groups = {GROUP_1_DN, PERM_1}) @Test - void should_GetAllTransferTargetsForSubjectUserAndGroup_When_QueryingForSinglePermission() { + void should_GetAllTransferTargetsForSubjectUserGroupPerm_When_QueryingForSinglePermission() { List results = WORKBASKET_SERVICE .createWorkbasketQuery() .callerHasPermissions(WorkbasketPermission.APPEND) .list(); - assertThat(results).hasSize(6); + assertThat(results).hasSize(7); } - @WithAccessId(user = "user-1-1", groups = GROUP_1_DN) + @WithAccessId(user = "user-1-1", groups = {GROUP_1_DN, PERM_1}) @Test - void should_GetAllTransferTargetsForSubjectUserAndGroup_When_QueryingForMultiplePermissions() { + void should_GetAllTransferTargetsForSubjectUserGroupPerm_When_QueryingForMultiplePermissions() { List results = WORKBASKET_SERVICE .createWorkbasketQuery() .callerHasPermissions(WorkbasketPermission.APPEND, WorkbasketPermission.OPEN) .list(); - assertThat(results).hasSize(4); + assertThat(results).hasSize(5); } @WithAccessId(user = "businessadmin") diff --git a/lib/taskana-core/src/test/java/acceptance/workbasket/query/WorkbasketQueryAccTest.java b/lib/taskana-core/src/test/java/acceptance/workbasket/query/WorkbasketQueryAccTest.java index 971ddd8619..d91a0bb836 100644 --- a/lib/taskana-core/src/test/java/acceptance/workbasket/query/WorkbasketQueryAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/workbasket/query/WorkbasketQueryAccTest.java @@ -31,7 +31,8 @@ void testQueryWorkbasketByUnauthenticated() { .createWorkbasketQuery() .nameLike("%") .accessIdsHavePermissions( - List.of(WorkbasketPermission.TRANSFER), "teamlead-1", GROUP_1_DN, GROUP_2_DN) + List.of(WorkbasketPermission.TRANSFER), + "teamlead-1", GROUP_1_DN, GROUP_2_DN, PERM_1) .list(); }; assertThatThrownBy(call).isInstanceOf(NotAuthorizedException.class); @@ -50,7 +51,8 @@ void testQueryWorkbasketByUnknownUser() { .createWorkbasketQuery() .nameLike("%") .accessIdsHavePermissions( - List.of(WorkbasketPermission.TRANSFER), "teamlead-1", GROUP_1_DN, GROUP_2_DN) + List.of(WorkbasketPermission.TRANSFER), + "teamlead-1", GROUP_1_DN, GROUP_2_DN, PERM_1) .list(); }; assertThatThrownBy(call).isInstanceOf(NotAuthorizedException.class); @@ -69,7 +71,8 @@ void testQueryWorkbasketByBusinessAdmin() throws Exception { .createWorkbasketQuery() .nameLike("%") .accessIdsHavePermissions( - List.of(WorkbasketPermission.TRANSFER), "teamlead-1", GROUP_1_DN, GROUP_2_DN) + List.of(WorkbasketPermission.TRANSFER), + "teamlead-1", GROUP_1_DN, GROUP_2_DN, PERM_1) .list(); assertThat(results).hasSize(13); @@ -88,7 +91,8 @@ void testQueryWorkbasketByAdmin() throws Exception { .createWorkbasketQuery() .nameLike("%") .accessIdsHavePermissions( - List.of(WorkbasketPermission.TRANSFER), "teamlead-1", GROUP_1_DN, GROUP_2_DN) + List.of(WorkbasketPermission.TRANSFER), + "teamlead-1", GROUP_1_DN, GROUP_2_DN, PERM_1) .list(); assertThat(results).hasSize(13); diff --git a/lib/taskana-test-api/src/main/java/pro/taskana/testapi/security/WithAccessId.java b/lib/taskana-test-api/src/main/java/pro/taskana/testapi/security/WithAccessId.java index 56ec82103a..87c0d7991e 100644 --- a/lib/taskana-test-api/src/main/java/pro/taskana/testapi/security/WithAccessId.java +++ b/lib/taskana-test-api/src/main/java/pro/taskana/testapi/security/WithAccessId.java @@ -15,6 +15,8 @@ String[] groups() default {}; + String[] permissions() default {}; + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface WithAccessIds { diff --git a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java index d98ae8dd99..9ba7089b58 100644 --- a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java +++ b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java @@ -38,6 +38,8 @@ public class BootWebSecurityConfigurer { private final String ldapUserDnPatterns; private final String ldapGroupSearchBase; private final String ldapGroupSearchFilter; + private final String ldapPermissionSearchBase; + private final String ldapPermissionSearchFilter; private final boolean devMode; private final boolean enableCsrf; @@ -48,6 +50,9 @@ public BootWebSecurityConfigurer( @Value("${taskana.ldap.userDnPatterns:uid={0},cn=users}") String ldapUserDnPatterns, @Value("${taskana.ldap.groupSearchBase:cn=groups}") String ldapGroupSearchBase, @Value("${taskana.ldap.groupSearchFilter:uniqueMember={0}}") String ldapGroupSearchFilter, + @Value("${taskana.ldap.permissionSearchBase:cn=permissions}") String ldapPermissionSearchBase, + @Value("${taskana.ldap.permissionSearchFilter:uniqueMember={0}}") + String ldapPermissionSearchFilter, @Value("${enableCsrf:false}") boolean enableCsrf, @Value("${devMode:false}") boolean devMode) { this.enableCsrf = enableCsrf; @@ -55,6 +60,8 @@ public BootWebSecurityConfigurer( this.ldapBaseDn = ldapBaseDn; this.ldapGroupSearchBase = ldapGroupSearchBase; this.ldapGroupSearchFilter = ldapGroupSearchFilter; + this.ldapPermissionSearchBase = ldapPermissionSearchBase; + this.ldapPermissionSearchFilter = ldapPermissionSearchFilter; this.ldapUserDnPatterns = ldapUserDnPatterns; this.devMode = devMode; } diff --git a/rest/taskana-rest-spring-example-boot/src/main/resources/application.properties b/rest/taskana-rest-spring-example-boot/src/main/resources/application.properties index a367722476..915da08e51 100644 --- a/rest/taskana-rest-spring-example-boot/src/main/resources/application.properties +++ b/rest/taskana-rest-spring-example-boot/src/main/resources/application.properties @@ -79,6 +79,12 @@ taskana.ldap.groupNameAttribute=cn taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 taskana.ldap.groupsOfUser=uniquemember +taskana.ldap.permissionSearchBase= +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true # Embedded Spring LDAP server spring.ldap.embedded.base-dn=OU=Test,O=TASKANA spring.ldap.embedded.credential.username=uid=admin diff --git a/rest/taskana-rest-spring-example-common/src/main/resources/pro/taskana/example/rest/controllers/taskana-customization.json b/rest/taskana-rest-spring-example-common/src/main/resources/pro/taskana/example/rest/controllers/taskana-customization.json index 19230ab8eb..005acbd6ab 100644 --- a/rest/taskana-rest-spring-example-common/src/main/resources/pro/taskana/example/rest/controllers/taskana-customization.json +++ b/rest/taskana-rest-spring-example-common/src/main/resources/pro/taskana/example/rest/controllers/taskana-customization.json @@ -1,5 +1,8 @@ { "EN": { + "global": { + "debounceTimeLookupField": 750 + }, "workbaskets": { "information": { "owner": { diff --git a/rest/taskana-rest-spring-example-common/src/main/resources/taskana-example.ldif b/rest/taskana-rest-spring-example-common/src/main/resources/taskana-example.ldif index cf8b00e506..ac5fcee192 100644 --- a/rest/taskana-rest-spring-example-common/src/main/resources/taskana-example.ldif +++ b/rest/taskana-rest-spring-example-common/src/main/resources/taskana-example.ldif @@ -11,6 +11,11 @@ cn: groups objectclass: top objectclass: container +dn: cn=permissions,OU=Test,O=TASKANA +cn: permissions +objectclass: top +objectclass: container + dn: cn=users,OU=Test,O=TASKANA cn: users objectclass: top @@ -109,8 +114,6 @@ sn: Toll ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 1 cn: Titus Toll userPassword: teamlead-1 -permission: organize -permission: inet dn: uid=user-1-1,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -126,8 +129,6 @@ sn: Mustermann ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 1 cn: Max Mustermann userPassword: user-1-1 -permission: organize -permission: inet dn: uid=user-1-2,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -137,15 +138,14 @@ objectclass: top givenName: Elena description: desc memberOf: cn=ksc-users,cn=groups,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter +permission: Taskana:CallCenter:AB:AB/A:CallCenter-vip memberOf: cn=Organisationseinheit KSC 1,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA uid: user-1-2 sn: Eifrig ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 1 cn: Elena Eifrig userPassword: user-1-2 -permission: organize -permission: inet -permission: program dn: uid=user-1-3,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -189,6 +189,7 @@ givenName: Simone description: desc memberOf: cn=Organisationseinheit KSC 2,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA memberOf: cn=ksc-users,cn=groups,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter uid: user-2-1 sn: Müller ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 2 @@ -223,9 +224,6 @@ sn: Bach ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 2 cn: Thomas Bach userPassword: user-2-3 -permission: organize -permission: inet -permission: program dn: uid=user-2-4,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -282,9 +280,6 @@ sn: Meyer ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 2 cn: Wiebke Meyer userPassword: user-2-7 -permission: organize -permission: inet -permission: manage dn: uid=user-2-8,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -370,10 +365,6 @@ sn: Bio ou: Organisationseinheit/Organisationseinheit B cn: Brunhilde Bio userPassword: user-b-2 -permission: organize -permission: inet -permission: siegen -permission: frieden ######################## # Users in other cn @@ -426,6 +417,25 @@ cn: monitor-users objectclass: groupofuniquenames objectclass: top +######################## +# Permissions +######################## + +dn: cn=g01,cn=groups,OU=Test,O=TASKANA +uniquemember: uid=user-1-2,cn=users,OU=Test,O=TASKANA +uniquemember: uid=user-2-1,cn=users,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter +cn: Taskana:CallCenter:AB:AB/A:CallCenter +objectclass: groupofuniquenames +objectclass: top + +dn: cn=g02,cn=groups,OU=Test,O=TASKANA +uniquemember: uid=user-1-2,cn=users,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter-vip +cn: g02 +objectclass: groupofuniquenames +objectclass: top + ###################### # Organizational Units ###################### diff --git a/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsForUseDnForGroupsDisabledTest.java b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsForUseDnForGroupsDisabledTest.java new file mode 100644 index 0000000000..96cdfab006 --- /dev/null +++ b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsForUseDnForGroupsDisabledTest.java @@ -0,0 +1,51 @@ +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 org.springframework.test.context.TestPropertySource; +import pro.taskana.common.rest.models.AccessIdRepresentationModel; +import pro.taskana.rest.test.TaskanaSpringBootTest; + +/** Test Ldap attachment. */ +@TaskanaSpringBootTest +@TestPropertySource(properties = "taskana.ldap.useDnForGroups=false") +@ActiveProfiles({"emptySearchRoots"}) +class LdapEmptySearchRootsForUseDnForGroupsDisabledTest + extends LdapForUseDnForGroupsDisabledTest { + + @Test + void should_FindGroupsForUser_When_UserIdIsProvided() throws Exception { + List groups = + ldapClient.searchGroupsAccessIdIsMemberOf("user-2-2"); + assertThat(groups) + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactlyInAnyOrder( + "ksc-users", "organisationseinheit ksc 2"); + } + + @Test + void should_FindPermissionsForUser_When_UserIdIsProvided() throws Exception { + List permissions = + ldapClient.searchPermissionsAccessIdHas("user-1-2"); + assertThat(permissions) + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactlyInAnyOrder("taskana:callcenter:ab:ab/a:callcenter", + "taskana:callcenter:ab:ab/a:callcenter-vip"); + } + + @Test + void should_ReturnFullDnForUser_When_AccessIdOfUserIsGiven() throws Exception { + String dn = ldapClient.searchDnForAccessId("otheruser"); + assertThat(dn).isEqualTo("uid=otheruser,cn=other-users,ou=test,o=taskana"); + } + + @Test + void should_ReturnFullDnForPermission_When_AccessIdOfPermissionIsGiven() throws Exception { + String dn = ldapClient.searchDnForAccessId("taskana:callcenter:ab:ab/a:callcenter-vip"); + assertThat(dn).isEqualTo("cn=g02,cn=groups,ou=test,o=taskana"); + } +} + diff --git a/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsTest.java b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsTest.java index 0928196661..657a6986cb 100644 --- a/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsTest.java +++ b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapEmptySearchRootsTest.java @@ -25,9 +25,25 @@ void should_FindGroupsForUser_When_UserIdIsProvided() throws Exception { + "cn=organisation,ou=test,o=taskana"); } + @Test + void should_FindPermissionsForUser_When_UserIdIsProvided() throws Exception { + List permissions = + ldapClient.searchPermissionsAccessIdHas("user-1-2"); + assertThat(permissions) + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactlyInAnyOrder("taskana:callcenter:ab:ab/a:callcenter", + "taskana:callcenter:ab:ab/a:callcenter-vip"); + } + @Test void should_ReturnFullDnForUser_When_AccessIdOfUserIsGiven() throws Exception { String dn = ldapClient.searchDnForAccessId("otheruser"); assertThat(dn).isEqualTo("uid=otheruser,cn=other-users,ou=test,o=taskana"); } + + @Test + void should_ReturnFullDnForPermission_When_AccessIdOfPermissionIsGiven() throws Exception { + String dn = ldapClient.searchDnForAccessId("taskana:callcenter:ab:ab/a:callcenter-vip"); + assertThat(dn).isEqualTo("cn=g02,cn=groups,ou=test,o=taskana"); + } } diff --git a/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapForUseDnForGroupsDisabledTest.java b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapForUseDnForGroupsDisabledTest.java new file mode 100644 index 0000000000..4e8bbe93bb --- /dev/null +++ b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapForUseDnForGroupsDisabledTest.java @@ -0,0 +1,110 @@ +package pro.taskana.example.ldap; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import pro.taskana.common.rest.ldap.LdapClient; +import pro.taskana.common.rest.models.AccessIdRepresentationModel; +import pro.taskana.rest.test.TaskanaSpringBootTest; +import pro.taskana.user.api.models.User; + +/** Test Ldap attachment. */ +@TestPropertySource(properties = "taskana.ldap.useDnForGroups=false") +@TaskanaSpringBootTest +class LdapForUseDnForGroupsDisabledTest { + + @Autowired + LdapClient ldapClient; + + @Test + void should_FindAllUsersAndGroupAndPermissions_When_SearchWithSubstringOfName() throws Exception { + List usersGroupsPermissions = + ldapClient.searchUsersAndGroupsAndPermissions("lead"); + assertThat(usersGroupsPermissions) + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactlyInAnyOrder( + "teamlead-1", "teamlead-2", "ksc-teamleads"); + } + + @Test + void should_FindUser_When_SearchingWithFirstAndLastname() throws Exception { + List usersGroupsPermissions = + ldapClient.searchUsersAndGroupsAndPermissions("Elena"); + assertThat(usersGroupsPermissions).hasSize(2); + + usersGroupsPermissions = ldapClient.searchUsersAndGroupsAndPermissions("Elena Faul"); + assertThat(usersGroupsPermissions).hasSize(1); + } + + @Test + void should_FindGroupsForUser_When_UserIdIsProvided() throws Exception { + List groups = + ldapClient.searchGroupsAccessIdIsMemberOf("user-2-2"); + assertThat(groups) + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactlyInAnyOrder("ksc-users"); + } + + @Test + void should_FindPermissionsForUser_When_UserIdIsProvided() throws Exception { + List permissions = + ldapClient.searchPermissionsAccessIdHas("user-1-2"); + assertThat(permissions) + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactlyInAnyOrder("taskana:callcenter:ab:ab/a:callcenter-vip", + "taskana:callcenter:ab:ab/a:callcenter"); + assertThat(permissions) + .extracting(AccessIdRepresentationModel::getName) + .containsExactlyInAnyOrder("Taskana:CallCenter:AB:AB/A:CallCenter-vip", + "Taskana:CallCenter:AB:AB/A:CallCenter"); + } + + @Test + void should_ReturnFullDnForUser_When_AccessIdOfUserIsGiven() throws Exception { + String dn = ldapClient.searchDnForAccessId("user-2-2"); + assertThat(dn).isEqualTo("uid=user-2-2,cn=users,ou=test,o=taskana"); + } + + @Test + void should_ReturnAllUsersInUserRoleWithCorrectAttributes() { + + Map users = + ldapClient.searchUsersInUserRole().stream() + .collect(Collectors.toMap(User::getId, Function.identity())); + + assertThat(users).hasSize(8); + + User teamlead1 = users.get("teamlead-1"); + assertThat(teamlead1.getId()).isEqualTo("teamlead-1"); + assertThat(teamlead1.getFirstName()).isEqualTo("Titus"); + assertThat(teamlead1.getLastName()).isEqualTo("Toll"); + assertThat(teamlead1.getFullName()).isEqualTo("Titus Toll"); + assertThat(teamlead1.getEmail()).isEqualTo("Titus.Toll@taskana.de"); + assertThat(teamlead1.getPhone()).isEqualTo("012345678"); + assertThat(teamlead1.getMobilePhone()).isEqualTo("09876554321"); + assertThat(teamlead1.getOrgLevel1()).isEqualTo("ABC"); + assertThat(teamlead1.getOrgLevel2()).isEqualTo("DEF/GHI"); + assertThat(teamlead1.getOrgLevel3()).isEqualTo("JKL"); + assertThat(teamlead1.getOrgLevel4()).isEqualTo("MNO/PQR"); + + User user11 = users.get("user-1-1"); + assertThat(user11.getId()).isEqualTo("user-1-1"); + assertThat(user11.getFirstName()).isEqualTo("Max"); + assertThat(user11.getLastName()).isEqualTo("Mustermann"); + assertThat(user11.getFullName()).isEqualTo("Max Mustermann"); + assertThat(user11.getEmail()).isNull(); + assertThat(user11.getPhone()).isNull(); + assertThat(user11.getMobilePhone()).isNull(); + assertThat(user11.getOrgLevel1()).isNull(); + assertThat(user11.getOrgLevel2()).isNull(); + assertThat(user11.getOrgLevel3()).isNull(); + assertThat(user11.getOrgLevel4()).isNull(); + } +} + diff --git a/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapTest.java b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapTest.java index 1360dad409..d44d68b50d 100644 --- a/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapTest.java +++ b/rest/taskana-rest-spring-example-common/src/test/java/pro/taskana/example/ldap/LdapTest.java @@ -20,9 +20,10 @@ class LdapTest { @Autowired LdapClient ldapClient; @Test - void should_FindAllUsersAndGroup_When_SearchWithSubstringOfName() throws Exception { - List usersAndGroups = ldapClient.searchUsersAndGroups("lead"); - assertThat(usersAndGroups) + void should_FindAllUsersAndGroupAndPermissions_When_SearchWithSubstringOfName() throws Exception { + List usersGroupsPermissions = + ldapClient.searchUsersAndGroupsAndPermissions("lead"); + assertThat(usersGroupsPermissions) .extracting(AccessIdRepresentationModel::getAccessId) .containsExactlyInAnyOrder( "teamlead-1", "teamlead-2", "cn=ksc-teamleads,cn=groups,ou=test,o=taskana"); @@ -30,11 +31,12 @@ void should_FindAllUsersAndGroup_When_SearchWithSubstringOfName() throws Excepti @Test void should_FindUser_When_SearchingWithFirstAndLastname() throws Exception { - List usersAndGroups = ldapClient.searchUsersAndGroups("Elena"); - assertThat(usersAndGroups).hasSize(2); + List usersGroupsPermissions = + ldapClient.searchUsersAndGroupsAndPermissions("Elena"); + assertThat(usersGroupsPermissions).hasSize(2); - usersAndGroups = ldapClient.searchUsersAndGroups("Elena Faul"); - assertThat(usersAndGroups).hasSize(1); + usersGroupsPermissions = ldapClient.searchUsersAndGroupsAndPermissions("Elena Faul"); + assertThat(usersGroupsPermissions).hasSize(1); } @Test @@ -46,6 +48,20 @@ void should_FindGroupsForUser_When_UserIdIsProvided() throws Exception { .containsExactlyInAnyOrder("cn=ksc-users,cn=groups,ou=test,o=taskana"); } + @Test + void should_FindPermissionsForUser_When_UserIdIsProvided() throws Exception { + List permissions = + ldapClient.searchPermissionsAccessIdHas("user-1-2"); + assertThat(permissions) + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactlyInAnyOrder("taskana:callcenter:ab:ab/a:callcenter-vip", + "taskana:callcenter:ab:ab/a:callcenter"); + assertThat(permissions) + .extracting(AccessIdRepresentationModel::getName) + .containsExactlyInAnyOrder("Taskana:CallCenter:AB:AB/A:CallCenter-vip", + "Taskana:CallCenter:AB:AB/A:CallCenter"); + } + @Test void should_ReturnFullDnForUser_When_AccessIdOfUserIsGiven() throws Exception { String dn = ldapClient.searchDnForAccessId("user-2-2"); diff --git a/rest/taskana-rest-spring-example-common/src/test/resources/application-emptySearchRoots.properties b/rest/taskana-rest-spring-example-common/src/test/resources/application-emptySearchRoots.properties index e1363f37ba..b152bad540 100644 --- a/rest/taskana-rest-spring-example-common/src/test/resources/application-emptySearchRoots.properties +++ b/rest/taskana-rest-spring-example-common/src/test/resources/application-emptySearchRoots.properties @@ -1,2 +1,3 @@ taskana.ldap.userSearchBase= taskana.ldap.groupSearchBase= +taskana.ldap.permissionSearchBase= diff --git a/rest/taskana-rest-spring-example-common/src/test/resources/application.properties b/rest/taskana-rest-spring-example-common/src/test/resources/application.properties index 133810e00a..70afb266e9 100644 --- a/rest/taskana-rest-spring-example-common/src/test/resources/application.properties +++ b/rest/taskana-rest-spring-example-common/src/test/resources/application.properties @@ -30,14 +30,21 @@ taskana.ldap.userOrglevel3Attribute=someDepartement taskana.ldap.userOrglevel4Attribute=orgLevel4 taskana.ldap.userIdAttribute=uid taskana.ldap.userMemberOfGroupAttribute=memberOf -taskana.ldap.userPermissionsAttribute=permission taskana.ldap.groupSearchBase=cn=groups taskana.ldap.groupSearchFilterName=objectclass taskana.ldap.groupSearchFilterValue=groupOfUniqueNames taskana.ldap.groupNameAttribute=cn +taskana.ldap.groupsOfUser=uniquemember taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 -taskana.ldap.groupsOfUser=uniquemember +taskana.ldap.userMemberOfPermissionAttribute=memberOf +taskana.ldap.permissionSearchBase= +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true +taskana.ldap.userPermissionsAttribute=permission # Embedded Spring LDAP server spring.ldap.embedded.base-dn=OU=Test,O=TASKANA spring.ldap.embedded.credential.username=uid=admin diff --git a/rest/taskana-rest-spring-example-wildfly/src/main/resources/application.properties b/rest/taskana-rest-spring-example-wildfly/src/main/resources/application.properties index 931f0a032d..581a2e6151 100644 --- a/rest/taskana-rest-spring-example-wildfly/src/main/resources/application.properties +++ b/rest/taskana-rest-spring-example-wildfly/src/main/resources/application.properties @@ -24,6 +24,12 @@ taskana.ldap.groupNameAttribute=cn taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 taskana.ldap.groupsOfUser=uniquemember +taskana.ldap.permissionSearchBase= +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true ####### JobScheduler cron expression that specifies when the JobSchedler runs taskana.jobscheduler.async.cron=0 * * * * * diff --git a/rest/taskana-rest-spring-example-wildfly/src/test/java/pro/taskana/example/wildfly/TaskanaWildflyWithUserConfigTest.java b/rest/taskana-rest-spring-example-wildfly/src/test/java/pro/taskana/example/wildfly/TaskanaWildflyWithUserConfigTest.java index b38521ea2c..f4cb66d84f 100644 --- a/rest/taskana-rest-spring-example-wildfly/src/test/java/pro/taskana/example/wildfly/TaskanaWildflyWithUserConfigTest.java +++ b/rest/taskana-rest-spring-example-wildfly/src/test/java/pro/taskana/example/wildfly/TaskanaWildflyWithUserConfigTest.java @@ -105,7 +105,7 @@ public void should_ReturnUserInformation_WhenCurrentUserNotAuthorizedToUseUserFr TaskanaUserInfoRepresentationModel currentUser = response.getBody(); assertThat(currentUser).isNotNull(); assertThat(currentUser.getUserId()).isEqualTo("user-2-1"); - assertThat(currentUser.getGroupIds()).hasSize(2); + assertThat(currentUser.getGroupIds()).hasSize(3); assertThat(currentUser.getRoles()).hasSize(1); } diff --git a/rest/taskana-rest-spring-example-wildfly/src/test/resources/application-with-additional-user-config.properties b/rest/taskana-rest-spring-example-wildfly/src/test/resources/application-with-additional-user-config.properties index 9763fd7b61..27cbbc265b 100644 --- a/rest/taskana-rest-spring-example-wildfly/src/test/resources/application-with-additional-user-config.properties +++ b/rest/taskana-rest-spring-example-wildfly/src/test/resources/application-with-additional-user-config.properties @@ -31,6 +31,13 @@ taskana.ldap.groupNameAttribute=cn taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 taskana.ldap.groupsOfUser=uniquemember +taskana.ldap.permissionSearchBase= +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true +taskana.ldap.userMemberOfPermissionAttribute=memberOf ####### JobScheduler cron expression that specifies when the JobSchedler runs taskana.jobscheduler.async.cron=0 * * * * * ####### cache static resources propertiesgit add -- diff --git a/rest/taskana-rest-spring-example-wildfly/src/test/resources/application.properties b/rest/taskana-rest-spring-example-wildfly/src/test/resources/application.properties index bd5ffd3d05..891a37ab54 100644 --- a/rest/taskana-rest-spring-example-wildfly/src/test/resources/application.properties +++ b/rest/taskana-rest-spring-example-wildfly/src/test/resources/application.properties @@ -29,6 +29,12 @@ taskana.ldap.groupNameAttribute=cn taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 taskana.ldap.groupsOfUser=uniquemember +taskana.ldap.permissionSearchBase= +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true ####### JobScheduler cron expression that specifies when the JobSchedler runs taskana.jobscheduler.async.cron=0 * * * * * ####### cache static resources propertiesgit add -- diff --git a/rest/taskana-rest-spring-example-wildfly/src/test/resources/taskana-test.ldif b/rest/taskana-rest-spring-example-wildfly/src/test/resources/taskana-test.ldif index 2c05e7969d..2d99bfde67 100644 --- a/rest/taskana-rest-spring-example-wildfly/src/test/resources/taskana-test.ldif +++ b/rest/taskana-rest-spring-example-wildfly/src/test/resources/taskana-test.ldif @@ -133,6 +133,8 @@ objectclass: top givenName: Elena description: desc memberOf: cn=ksc-users,cn=groups,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter +permission: Taskana:CallCenter:AB:AB/A:CallCenter-vip memberOf: cn=Organisationseinheit KSC 1,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA uid: user-1-2 sn: Eifrig @@ -182,6 +184,7 @@ givenName: Simone description: desc memberOf: cn=Organisationseinheit KSC 2,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA memberOf: cn=ksc-users,cn=groups,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter uid: user-2-1 sn: Müller ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 2 @@ -409,6 +412,25 @@ cn: monitor-users objectclass: groupofuniquenames objectclass: top +######################## +# Permissions +######################## + +dn: cn=g01,cn=groups,OU=Test,O=TASKANA +uniquemember: uid=user-1-2,cn=users,OU=Test,O=TASKANA +uniquemember: uid=user-2-1,cn=users,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter +cn: Taskana:CallCenter:AB:AB/A:CallCenter +objectclass: groupofuniquenames +objectclass: top + +dn: cn=g02,cn=groups,OU=Test,O=TASKANA +uniquemember: uid=user-1-2,cn=users,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter-vip +cn: g02 +objectclass: groupofuniquenames +objectclass: top + ###################### # Organizational Units ###################### diff --git a/rest/taskana-rest-spring-test-lib/src/main/java/pro/taskana/rest/test/TestWebSecurityConfig.java b/rest/taskana-rest-spring-test-lib/src/main/java/pro/taskana/rest/test/TestWebSecurityConfig.java index d6f3c02146..1e8b5b3c96 100644 --- a/rest/taskana-rest-spring-test-lib/src/main/java/pro/taskana/rest/test/TestWebSecurityConfig.java +++ b/rest/taskana-rest-spring-test-lib/src/main/java/pro/taskana/rest/test/TestWebSecurityConfig.java @@ -48,7 +48,10 @@ public TestWebSecurityConfig( @Value("${taskana.ldap.baseDn:OU=Test,O=TASKANA}") String ldapBaseDn, @Value("${taskana.ldap.userDnPatterns:uid={0},cn=users}") String ldapUserDnPatterns, @Value("${taskana.ldap.groupSearchBase:cn=groups}") String ldapGroupSearchBase, - @Value("${taskana.ldap.groupSearchFilter:uniqueMember={0}}") String ldapGroupSearchFilter) { + @Value("${taskana.ldap.groupSearchFilter:uniqueMember={0}}") String ldapGroupSearchFilter, + @Value("${taskana.ldap.permissionSearchBase:cn=groups}") String ldapPermissionSearchBase, + @Value("${taskana.ldap.permissionSearchFilter:uniqueMember={0}}") + String ldapPermissionSearchFilter) { this.ldapServerUrl = ldapServerUrl; this.ldapBaseDn = ldapBaseDn; this.ldapUserDnPatterns = ldapUserDnPatterns; diff --git a/rest/taskana-rest-spring-test-lib/src/main/resources/taskana-test.ldif b/rest/taskana-rest-spring-test-lib/src/main/resources/taskana-test.ldif index cf8b00e506..99bfaf7d0d 100644 --- a/rest/taskana-rest-spring-test-lib/src/main/resources/taskana-test.ldif +++ b/rest/taskana-rest-spring-test-lib/src/main/resources/taskana-test.ldif @@ -11,6 +11,11 @@ cn: groups objectclass: top objectclass: container +dn: cn=permissions,OU=Test,O=TASKANA +cn: permissions +objectclass: top +objectclass: container + dn: cn=users,OU=Test,O=TASKANA cn: users objectclass: top @@ -109,8 +114,6 @@ sn: Toll ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 1 cn: Titus Toll userPassword: teamlead-1 -permission: organize -permission: inet dn: uid=user-1-1,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -126,8 +129,6 @@ sn: Mustermann ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 1 cn: Max Mustermann userPassword: user-1-1 -permission: organize -permission: inet dn: uid=user-1-2,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -137,15 +138,14 @@ objectclass: top givenName: Elena description: desc memberOf: cn=ksc-users,cn=groups,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter +permission: Taskana:CallCenter:AB:AB/A:CallCenter-vip memberOf: cn=Organisationseinheit KSC 1,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA uid: user-1-2 sn: Eifrig ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 1 cn: Elena Eifrig userPassword: user-1-2 -permission: organize -permission: inet -permission: program dn: uid=user-1-3,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -189,6 +189,7 @@ givenName: Simone description: desc memberOf: cn=Organisationseinheit KSC 2,cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA memberOf: cn=ksc-users,cn=groups,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter uid: user-2-1 sn: Müller ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 2 @@ -223,9 +224,6 @@ sn: Bach ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 2 cn: Thomas Bach userPassword: user-2-3 -permission: organize -permission: inet -permission: program dn: uid=user-2-4,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -282,9 +280,6 @@ sn: Meyer ou: Organisationseinheit/Organisationseinheit KSC/Organisationseinheit KSC 2 cn: Wiebke Meyer userPassword: user-2-7 -permission: organize -permission: inet -permission: manage dn: uid=user-2-8,cn=users,OU=Test,O=TASKANA objectclass: inetorgperson @@ -370,10 +365,6 @@ sn: Bio ou: Organisationseinheit/Organisationseinheit B cn: Brunhilde Bio userPassword: user-b-2 -permission: organize -permission: inet -permission: siegen -permission: frieden ######################## # Users in other cn @@ -426,6 +417,25 @@ cn: monitor-users objectclass: groupofuniquenames objectclass: top +######################## +# Permissions +######################## + +dn: cn=g01,cn=groups,OU=Test,O=TASKANA +uniquemember: uid=user-1-2,cn=users,OU=Test,O=TASKANA +uniquemember: uid=user-2-1,cn=users,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter +cn: g01 +objectclass: groupofuniquenames +objectclass: top + +dn: cn=g02,cn=groups,OU=Test,O=TASKANA +uniquemember: uid=user-1-2,cn=users,OU=Test,O=TASKANA +permission: Taskana:CallCenter:AB:AB/A:CallCenter-vip +cn: g02 +objectclass: groupofuniquenames +objectclass: top + ###################### # Organizational Units ###################### diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/AccessIdController.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/AccessIdController.java index 61cfd52a1b..e6cc5ca098 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/AccessIdController.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/AccessIdController.java @@ -1,6 +1,7 @@ package pro.taskana.common.rest; import java.util.List; +import javax.naming.InvalidNameException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; @@ -37,15 +38,17 @@ public AccessIdController(LdapClient ldapClient, TaskanaEngine taskanaEngine) { * @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) + * @throws InvalidNameException if name is not a valid dn. + * @title Search for Access Id (users and groups and permissions) */ @GetMapping(path = RestEndpoints.URL_ACCESS_ID) - public ResponseEntity> searchUsersAndGroups( + public ResponseEntity> searchUsersAndGroupsAndPermissions( @RequestParam("search-for") String searchFor) - throws InvalidArgumentException, NotAuthorizedException { + throws InvalidArgumentException, NotAuthorizedException, InvalidNameException { taskanaEngine.checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.BUSINESS_ADMIN); - List accessIdUsers = ldapClient.searchUsersAndGroups(searchFor); + List accessIdUsers = + ldapClient.searchUsersAndGroupsAndPermissions(searchFor); return ResponseEntity.ok(accessIdUsers); } @@ -88,12 +91,13 @@ public ResponseEntity> searchUsersByNameOrAcce * @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. + * @throws InvalidNameException if name is not a valid dn. * @title Get groups for Access Id */ @GetMapping(path = RestEndpoints.URL_ACCESS_ID_GROUPS) public ResponseEntity> getGroupsByAccessId( @RequestParam("access-id") String accessId) - throws InvalidArgumentException, NotAuthorizedException { + throws InvalidArgumentException, NotAuthorizedException, InvalidNameException { taskanaEngine.checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.BUSINESS_ADMIN); List accessIds = @@ -101,4 +105,26 @@ public ResponseEntity> getGroupsByAccessId( return ResponseEntity.ok(accessIds); } + + /** + * This endpoint retrieves all permissions a given Access Id belongs to. + * + * @param accessId the Access Id whose permissions should be determined. + * @return a list of the permission 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. + * @throws InvalidNameException if name is not a valid dn. + * @title Get permissions for Access Id + */ + @GetMapping(path = RestEndpoints.URL_ACCESS_ID_PERMISSIONS) + public ResponseEntity> getPermissionsByAccessId( + @RequestParam("access-id") String accessId) + throws InvalidArgumentException, NotAuthorizedException, InvalidNameException { + taskanaEngine.checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.BUSINESS_ADMIN); + + List accessIds = + ldapClient.searchPermissionsAccessIdHas(accessId); + + return ResponseEntity.ok(accessIds); + } } diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestEndpoints.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestEndpoints.java index 25e0c7f711..e1a2fc2ab4 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestEndpoints.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/RestEndpoints.java @@ -20,6 +20,7 @@ public final class RestEndpoints { public static final String URL_ACCESS_ID = API_V1 + "access-ids"; public static final String URL_ACCESS_ID_WITH_NAME = URL_ACCESS_ID + "/with-name"; public static final String URL_ACCESS_ID_GROUPS = URL_ACCESS_ID + "/groups"; + public static final String URL_ACCESS_ID_PERMISSIONS = URL_ACCESS_ID + "/permissions"; // import / export endpoints public static final String URL_CLASSIFICATION_DEFINITIONS = API_V1 + "classification-definitions"; diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapClient.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapClient.java index 45c1dd3eec..f83e63da35 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapClient.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapClient.java @@ -7,13 +7,17 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.naming.InvalidNameException; import javax.naming.directory.SearchControls; +import javax.naming.ldap.LdapName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -23,7 +27,9 @@ import org.springframework.ldap.core.support.AbstractContextMapper; import org.springframework.ldap.filter.AndFilter; import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.filter.NotPresentFilter; import org.springframework.ldap.filter.OrFilter; +import org.springframework.ldap.filter.PresentFilter; import org.springframework.ldap.filter.WhitespaceWildcardsFilter; import org.springframework.ldap.support.LdapNameBuilder; import org.springframework.stereotype.Component; @@ -62,15 +68,16 @@ public LdapClient( } /** - * Search LDAP for matching users or groups. + * Search LDAP for matching users or groups or permissions. * - * @param name lookup string for names or groups + * @param name lookup string for names or groups or permissions * @return a list of AccessIdResources sorted by AccessId and limited to * maxNumberOfReturnedAccessIds * @throws InvalidArgumentException if input is shorter than minSearchForLength + * @throws InvalidNameException thrown if name is not a valid dn */ - public List searchUsersAndGroups(final String name) - throws InvalidArgumentException { + public List searchUsersAndGroupsAndPermissions(final String name) + throws InvalidArgumentException, InvalidNameException { isInitOrFail(); testMinSearchForLength(name); @@ -83,6 +90,7 @@ public List searchUsersAndGroups(final String name) } else { accessIds.addAll(searchUsersByNameOrAccessId(name)); accessIds.addAll(searchGroupsByName(name)); + accessIds.addAll(searchPermissionsByName(name)); } sortListOfAccessIdResources(accessIds); return getFirstPageOfaResultList(accessIds); @@ -214,19 +222,74 @@ public List searchGroupsByName(final String name) if (!CN.equals(getGroupNameAttribute())) { orFilter.or(new WhitespaceWildcardsFilter(CN, name)); } + final AndFilter andFilter2 = new AndFilter(); + andFilter2.and(new NotPresentFilter(getUserPermissionsAttribute())); andFilter.and(orFilter); + andFilter2.and(andFilter); LOGGER.debug("Using filter '{}' for LDAP query.", andFilter); return ldapTemplate.search( getGroupSearchBase(), - andFilter.encode(), + andFilter2.encode(), SearchControls.SUBTREE_SCOPE, getLookUpGroupAttributesToReturn(), new GroupContextMapper()); } - public AccessIdRepresentationModel searchAccessIdByDn(final String dn) { + public Map> searchAccessIdForGroupsAndPermissionsByDn(List dns) + throws InvalidNameException { + List accessIdsOfGroupsAndPermissions = new ArrayList<>(); + List permissions = new ArrayList<>(); + List accessIdsOfPermissions = new ArrayList<>(); + for (String groupOrPermission : dns) { + accessIdsOfGroupsAndPermissions.add(searchAccessIdByDn(groupOrPermission) + .getAccessId()); + } + for (String groupOrPermission : accessIdsOfGroupsAndPermissions) { + permissions.addAll(searchPermissionsByName(groupOrPermission)); + } + for (AccessIdRepresentationModel permission : permissions) { + accessIdsOfPermissions.add(permission.getAccessId()); + } + List accessIdsOfGroups = new ArrayList<>(accessIdsOfGroupsAndPermissions); + accessIdsOfGroups.removeAll(accessIdsOfPermissions); + Map> map = new HashMap<>(); + map.put("permissions", accessIdsOfPermissions); + map.put("groups", accessIdsOfGroups); + return map; + } + + public List searchPermissionsByName(final String name) + throws InvalidArgumentException { + isInitOrFail(); + testMinSearchForLength(name); + + final AndFilter andFilter = new AndFilter(); + andFilter.and(new EqualsFilter(getPermissionSearchFilterName(), + getPermissionSearchFilterValue())); + final OrFilter orFilter = new OrFilter(); + orFilter.or(new WhitespaceWildcardsFilter(getUserPermissionsAttribute(), name)); + if (!CN.equals(getUserPermissionsAttribute())) { + orFilter.or(new WhitespaceWildcardsFilter(CN, name)); + } + final AndFilter andFilter2 = new AndFilter(); + andFilter2.and(new PresentFilter(getUserPermissionsAttribute())); + andFilter.and(orFilter); + andFilter2.and(andFilter); + + LOGGER.debug("Using filter '{}' for LDAP query.", andFilter); + + return ldapTemplate.search( + getPermissionSearchBase(), + andFilter2.encode(), + SearchControls.SUBTREE_SCOPE, + getLookUpPermissionAttributesToReturn(), + new PermissionContextMapper()); + } + + public AccessIdRepresentationModel searchAccessIdByDn(final String dn) + throws InvalidNameException { isInitOrFail(); // Obviously Spring LdapTemplate does have a inconsistency and always adds the base name to the // given DN. @@ -239,11 +302,13 @@ public AccessIdRepresentationModel searchAccessIdByDn(final String dn) { "Removed baseDN {} from given DN. New DN to be used: {}", getBaseDn(), nameWithoutBaseDn); } return ldapTemplate.lookup( - nameWithoutBaseDn, getLookUpUserAndGroupAttributesToReturn(), new DnContextMapper()); + new LdapName(nameWithoutBaseDn), + getLookUpUserAndGroupAndPermissionAttributesToReturn(), + new DnContextMapper()); } public List searchGroupsAccessIdIsMemberOf(final String accessId) - throws InvalidArgumentException { + throws InvalidArgumentException, InvalidNameException { isInitOrFail(); testMinSearchForLength(accessId); @@ -259,74 +324,186 @@ public List searchGroupsAccessIdIsMemberOf(final St orFilter.or(new EqualsFilter(getGroupsOfUserName(), accessId)); } orFilter.or(new EqualsFilter(getGroupsOfUserName(), dn)); + final AndFilter andFilter2 = new AndFilter(); + andFilter2.and(new NotPresentFilter(getUserPermissionsAttribute())); andFilter.and(orFilter); + andFilter2.and(andFilter); String[] userAttributesToReturn = {getUserIdAttribute(), getGroupNameAttribute()}; if (LOGGER.isDebugEnabled()) { LOGGER.debug( "Using filter '{}' for LDAP query with group search base {}.", - andFilter, + andFilter2, getGroupSearchBase()); } return ldapTemplate.search( getGroupSearchBase(), - andFilter.encode(), + andFilter2.encode(), SearchControls.SUBTREE_SCOPE, userAttributesToReturn, new GroupContextMapper()); } + public List searchPermissionsAccessIdHas(final String accessId) + throws InvalidArgumentException, InvalidNameException { + 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(getPermissionSearchFilterName(), + getPermissionSearchFilterValue())); + final OrFilter orFilter = new OrFilter(); + if (!"DN".equalsIgnoreCase(getPermissionsOfUserType())) { + orFilter.or(new EqualsFilter(getPermissionsOfUserName(), accessId)); + } + orFilter.or(new EqualsFilter(getPermissionsOfUserName(), dn)); + final AndFilter andFilter2 = new AndFilter(); + andFilter2.and(new PresentFilter(getUserPermissionsAttribute())); + andFilter.and(orFilter); + andFilter2.and(andFilter); + + String[] userAttributesToReturn = {getUserIdAttribute(), getUserPermissionsAttribute()}; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Using filter '{}' for LDAP query with group search base {}.", + andFilter2, + getPermissionSearchBase()); + } + + return ldapTemplate.search( + getPermissionSearchBase(), + andFilter2.encode(), + SearchControls.SUBTREE_SCOPE, + userAttributesToReturn, + new PermissionContextMapper()); + } + /** * Performs a lookup to retrieve correct DN for the given access id. * * @param accessId The access id to lookup * @return the LDAP Distinguished Name for the access id * @throws InvalidArgumentException thrown if the given access id is ambiguous. + * @throws InvalidNameException thrown if name is not a valid dn */ - public String searchDnForAccessId(String accessId) throws InvalidArgumentException { + public String searchDnForAccessId(String accessId) + throws InvalidArgumentException, InvalidNameException { 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 distinguishedNames = - ldapTemplate.search( - getUserSearchBase(), - andFilter.encode(), - SearchControls.SUBTREE_SCOPE, - null, - new DnStringContextMapper()); + return searchDnForAccessIdIfAccessIdNameIsNotDn(accessId); + } + } - if (distinguishedNames == null || distinguishedNames.isEmpty()) { - return null; - } else if (distinguishedNames.size() > 1) { + private String searchDnForAccessIdIfAccessIdNameIsNotDn(String accessId) { + final List distinguishedNames = searchDnForUserAccessId(accessId); + if (distinguishedNames == null || distinguishedNames.isEmpty()) { + final List distinguishedNamesPermissions = searchDnForPermissionAccessId(accessId); + if (distinguishedNamesPermissions == null || distinguishedNamesPermissions.isEmpty()) { + final List distinguishedNamesGroups = searchDnForGroupAccessId(accessId); + if (distinguishedNamesGroups == null || distinguishedNamesGroups.isEmpty()) { + return null; + } else if (distinguishedNamesGroups.size() > 1) { + throw new InvalidArgumentException("Ambiguous access id found: " + accessId); + } else { + return distinguishedNamesGroups.get(0); + } + } else if (distinguishedNamesPermissions.size() > 1) { throw new InvalidArgumentException("Ambiguous access id found: " + accessId); } else { - return distinguishedNames.get(0); + return distinguishedNamesPermissions.get(0); } + } else if (distinguishedNames.size() > 1) { + throw new InvalidArgumentException("Ambiguous access id found: " + accessId); + } else { + return distinguishedNames.get(0); } } + private List searchDnForUserAccessId(String accessId) { + 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()); + + return ldapTemplate.search( + getUserSearchBase(), + andFilter.encode(), + SearchControls.SUBTREE_SCOPE, + null, + new DnStringContextMapper()); + } + + private List searchDnForPermissionAccessId(String accessId) { + final AndFilter andFilter = new AndFilter(); + andFilter.and(new EqualsFilter(getPermissionSearchFilterName(), + getPermissionSearchFilterValue())); + final OrFilter orFilter = new OrFilter(); + orFilter.or(new EqualsFilter(getUserPermissionsAttribute(), accessId)); + final AndFilter andFilterPermission2 = new AndFilter(); + andFilter.and(new PresentFilter(getUserPermissionsAttribute())); + andFilter.and(orFilter); + andFilterPermission2.and(andFilter); + + LOGGER.debug( + "Using filter '{}' for LDAP query with user search base {}.", + andFilterPermission2, + getPermissionSearchBase()); + + return ldapTemplate.search( + getPermissionSearchBase(), + andFilterPermission2.encode(), + SearchControls.SUBTREE_SCOPE, + null, + new DnStringContextMapper()); + } + + private List searchDnForGroupAccessId(String accessId) { + final AndFilter andFilter = new AndFilter(); + andFilter.and(new EqualsFilter(getGroupSearchFilterName(), getGroupSearchFilterValue())); + final OrFilter orFilter = new OrFilter(); + orFilter.or(new EqualsFilter(getGroupNameAttribute(), accessId)); + final AndFilter andFilter2 = new AndFilter(); + andFilter2.and(new NotPresentFilter(getUserPermissionsAttribute())); + andFilter.and(orFilter); + andFilter2.and(andFilter); + + LOGGER.debug( + "Using filter '{}' for LDAP query with user search base {}.", + andFilter2, + getPermissionSearchBase()); + + return ldapTemplate.search( + getPermissionSearchBase(), + andFilter.encode(), + SearchControls.SUBTREE_SCOPE, + null, + new DnStringContextMapper()); + } + /** * Validates a given AccessId / name. * * @param name lookup string for names or groups * @return whether the given name is valid or not + * @throws InvalidNameException thrown if name is not a valid dn */ - public boolean validateAccessId(final String name) { + public boolean validateAccessId(final String name) throws InvalidNameException { isInitOrFail(); if (nameIsDn(name)) { @@ -417,6 +594,22 @@ public String getUserPermissionsAttribute() { return LdapSettings.TASKANA_LDAP_USER_PERMISSIONS_ATTRIBUTE.getValueFromEnv(env); } + public String getPermissionSearchBase() { + return LdapSettings.TASKANA_LDAP_PERMISSION_SEARCH_BASE.getValueFromEnv(env); + } + + public String getPermissionSearchFilterName() { + return LdapSettings.TASKANA_LDAP_PERMISSION_SEARCH_FILTER_NAME.getValueFromEnv(env); + } + + public String getPermissionSearchFilterValue() { + return LdapSettings.TASKANA_LDAP_PERMISSION_SEARCH_FILTER_VALUE.getValueFromEnv(env); + } + + public String getPermissionNameAttribute() { + return LdapSettings.TASKANA_LDAP_PERMISSION_NAME_ATTRIBUTE.getValueFromEnv(env); + } + public String getGroupSearchBase() { return LdapSettings.TASKANA_LDAP_GROUP_SEARCH_BASE.getValueFromEnv(env); } @@ -458,6 +651,15 @@ public int calcMaxNumberOfReturnedAccessIds(int defaultValue) { return Integer.parseInt(envValue); } + public boolean useDnForGroups() { + String envValue = + LdapSettings.TASKANA_LDAP_USE_DN_FOR_GROUPS.getValueFromEnv(env); + if (envValue == null || envValue.isEmpty()) { + return true; + } + return Boolean.parseBoolean(envValue); + } + public int getMaxNumberOfReturnedAccessIds() { return maxNumberOfReturnedAccessIds; } @@ -474,6 +676,19 @@ public String getGroupsOfUserType() { return LdapSettings.TASKANA_LDAP_GROUPS_OF_USER_TYPE.getValueFromEnv(env); } + public String getPermissionsOfUserName() { + String permissionsOfUser = + LdapSettings.TASKANA_LDAP_PERMISSIONS_OF_USER_NAME.getValueFromEnv(env); + if (permissionsOfUser == null || permissionsOfUser.isEmpty()) { + permissionsOfUser = LdapSettings.TASKANA_LDAP_PERMISSIONS_OF_USER.getValueFromEnv(env); + } + return permissionsOfUser; + } + + public String getPermissionsOfUserType() { + return LdapSettings.TASKANA_LDAP_PERMISSIONS_OF_USER_TYPE.getValueFromEnv(env); + } + public boolean isUser(String accessId) { return !getUsersByAccessId(accessId).isEmpty(); } @@ -522,10 +737,19 @@ String[] getLookUpGroupAttributesToReturn() { return new String[] {getGroupNameAttribute(), CN, getGroupSearchFilterName()}; } - String[] getLookUpUserAndGroupAttributesToReturn() { - return Stream.concat( + String[] getLookUpPermissionAttributesToReturn() { + return new String[] { + getUserPermissionsAttribute(), + getPermissionSearchFilterName() + }; + } + + String[] getLookUpUserAndGroupAndPermissionAttributesToReturn() { + return Stream.concat(Stream.concat( Arrays.stream(getLookUpUserAttributesToReturn()), - Arrays.stream(getLookUpGroupAttributesToReturn())) + Arrays.stream(getLookUpGroupAttributesToReturn())), + Arrays.stream(getLookUpPermissionAttributesToReturn()) + ) .toArray(String[]::new); } @@ -587,6 +811,9 @@ List checkForMissingConfigurations() { .filter(not(LdapSettings.TASKANA_LDAP_GROUPS_OF_USER::equals)) .filter(not(LdapSettings.TASKANA_LDAP_GROUPS_OF_USER_NAME::equals)) .filter(not(LdapSettings.TASKANA_LDAP_GROUPS_OF_USER_TYPE::equals)) + .filter(not(LdapSettings.TASKANA_LDAP_PERMISSIONS_OF_USER::equals)) + .filter(not(LdapSettings.TASKANA_LDAP_PERMISSIONS_OF_USER_NAME::equals)) + .filter(not(LdapSettings.TASKANA_LDAP_PERMISSIONS_OF_USER_TYPE::equals)) .filter(p -> p.getValueFromEnv(env) == null) .toList(); } @@ -622,6 +849,15 @@ private String getUserIdFromContext(final DirContextOperations context) { } } + private String getGroupIdFromContext(final DirContextOperations context) { + String groupId = context.getStringAttribute(getGroupNameAttribute()); + if (groupId != null && useLowerCaseForAccessIds) { + return groupId.toLowerCase(); + } else { + return groupId; + } + } + private Set getGroupIdsFromContext(final DirContextOperations context) { String[] groupAttributes = context.getStringAttributes(getUserMemberOfGroupAttribute()); Set groups = groupAttributes != null ? Set.of(groupAttributes) : Collections.emptySet(); @@ -635,6 +871,15 @@ private Set getGroupIdsFromContext(final DirContextOperations context) { return groups; } + private String getPermissionIdFromContext(final DirContextOperations context) { + String permissionId = context.getStringAttribute(getUserPermissionsAttribute()); + if (permissionId != null && useLowerCaseForAccessIds) { + return permissionId.toLowerCase(); + } else { + return permissionId; + } + } + private Set getPermissionIdsFromContext(final DirContextOperations context) { String[] permissionAttributes = context.getStringAttributes(getUserPermissionsAttribute()); Set permissions = @@ -655,12 +900,27 @@ class GroupContextMapper extends AbstractContextMapper { + + @Override + public AccessIdRepresentationModel doMapFromContext(final DirContextOperations context) { + final AccessIdRepresentationModel accessId = new AccessIdRepresentationModel(); + accessId.setAccessId(getPermissionIdFromContext(context)); // fully qualified dn + accessId.setName(context.getStringAttribute(getUserPermissionsAttribute())); + return accessId; + } + } + /** Context Mapper for user info entries. */ class UserInfoContextMapper extends AbstractContextMapper { @@ -712,9 +972,16 @@ public AccessIdRepresentationModel doMapFromContext(final DirContextOperations c String firstName = context.getStringAttribute(getUserFirstnameAttribute()); String lastName = context.getStringAttribute(getUserLastnameAttribute()); accessId.setName(String.format("%s, %s", lastName, firstName)); - } else { - accessId.setAccessId(getDnFromContext(context)); // fully qualified dn + } else if (context.getStringAttribute(getUserPermissionsAttribute()) == null) { + if (useDnForGroups()) { + accessId.setAccessId(getDnFromContext(context)); // fully qualified dn + } else { + accessId.setAccessId(getGroupIdFromContext(context)); + } accessId.setName(context.getStringAttribute(getGroupNameAttribute())); + } else { + accessId.setAccessId(getPermissionIdFromContext(context)); + accessId.setName(context.getStringAttribute(getUserPermissionsAttribute())); } return accessId; } diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapSettings.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapSettings.java index 7e7347f9d2..88adfcc446 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapSettings.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/ldap/LdapSettings.java @@ -20,6 +20,10 @@ enum LdapSettings { TASKANA_LDAP_USER_ORG_LEVEL_4_ATTRIBUTE("taskana.ldap.userOrglevel4Attribute"), TASKANA_LDAP_USER_MEMBER_OF_GROUP_ATTRIBUTE("taskana.ldap.userMemberOfGroupAttribute"), TASKANA_LDAP_USER_PERMISSIONS_ATTRIBUTE("taskana.ldap.userPermissionsAttribute"), + TASKANA_LDAP_PERMISSION_SEARCH_BASE("taskana.ldap.permissionSearchBase"), + TASKANA_LDAP_PERMISSION_SEARCH_FILTER_NAME("taskana.ldap.permissionSearchFilterName"), + TASKANA_LDAP_PERMISSION_SEARCH_FILTER_VALUE("taskana.ldap.permissionSearchFilterValue"), + TASKANA_LDAP_PERMISSION_NAME_ATTRIBUTE("taskana.ldap.permissionNameAttribute"), TASKANA_LDAP_GROUP_SEARCH_BASE("taskana.ldap.groupSearchBase"), TASKANA_LDAP_BASE_DN("taskana.ldap.baseDn"), TASKANA_LDAP_GROUP_SEARCH_FILTER_NAME("taskana.ldap.groupSearchFilterName"), @@ -29,7 +33,11 @@ enum LdapSettings { TASKANA_LDAP_MAX_NUMBER_OF_RETURNED_ACCESS_IDS("taskana.ldap.maxNumberOfReturnedAccessIds"), TASKANA_LDAP_GROUPS_OF_USER("taskana.ldap.groupsOfUser"), TASKANA_LDAP_GROUPS_OF_USER_NAME("taskana.ldap.groupsOfUser.name"), - TASKANA_LDAP_GROUPS_OF_USER_TYPE("taskana.ldap.groupsOfUser.type"); + TASKANA_LDAP_GROUPS_OF_USER_TYPE("taskana.ldap.groupsOfUser.type"), + TASKANA_LDAP_PERMISSIONS_OF_USER("taskana.ldap.permissionsOfUser"), + TASKANA_LDAP_PERMISSIONS_OF_USER_NAME("taskana.ldap.permissionsOfUser.name"), + TASKANA_LDAP_PERMISSIONS_OF_USER_TYPE("taskana.ldap.permissionsOfUser.type"), + TASKANA_LDAP_USE_DN_FOR_GROUPS("taskana.ldap.useDnForGroups"); private final String key; diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/models/TaskanaUserInfoRepresentationModel.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/models/TaskanaUserInfoRepresentationModel.java index 4fac727db4..2cc397982e 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/models/TaskanaUserInfoRepresentationModel.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/models/TaskanaUserInfoRepresentationModel.java @@ -14,7 +14,7 @@ public class TaskanaUserInfoRepresentationModel private String userId; /** All groups the current user is a member of. */ private List groupIds = new ArrayList<>(); - /** All taskana roles the current user fulfills. */ + /** All permissions the current user has. */ private List roles = new ArrayList<>(); public String getUserId() { diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerForUseDnForGroupsDisabledIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerForUseDnForGroupsDisabledIntTest.java new file mode 100644 index 0000000000..1fc7f1c84a --- /dev/null +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerForUseDnForGroupsDisabledIntTest.java @@ -0,0 +1,280 @@ +package pro.taskana.common.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static pro.taskana.rest.test.RestHelper.TEMPLATE; + +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.client.HttpStatusCodeException; +import pro.taskana.common.internal.util.Pair; +import pro.taskana.common.rest.models.AccessIdRepresentationModel; +import pro.taskana.rest.test.RestHelper; +import pro.taskana.rest.test.TaskanaSpringBootTest; + +@TestPropertySource(properties = "taskana.ldap.useDnForGroups=false") +@TaskanaSpringBootTest +class AccessIdControllerForUseDnForGroupsDisabledIntTest { + + private static final ParameterizedTypeReference> + ACCESS_ID_LIST_TYPE = new ParameterizedTypeReference>() {}; + + private final RestHelper restHelper; + + @Autowired + AccessIdControllerForUseDnForGroupsDisabledIntTest(RestHelper restHelper) { + this.restHelper = restHelper; + } + + @TestFactory + Stream should_ResolveAccessId_When_SearchingForDnOrCn() { + List> list = + List.of( + Pair.of( + "cn=ksc-users,cn=groups,OU=Test,O=TASKANA", + "ksc-users"), + Pair.of("uid=teamlead-1,cn=users,OU=Test,O=TASKANA", "teamlead-1"), + Pair.of("ksc-use", "ksc-users"), + Pair.of("user-b-2", "user-b-2"), + Pair.of("User-b-2", "user-b-2"), + Pair.of("cn=g01,cn=groups,OU=Test,O=TASKANA", + "taskana:callcenter:ab:ab/a:callcenter"), + Pair.of("cn=g02,cn=groups,OU=Test,O=TASKANA", + "taskana:callcenter:ab:ab/a:callcenter-vip")); + + ThrowingConsumer> test = + pair -> { + String url = + restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=" + pair.getLeft(); + HttpEntity auth = + new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .containsExactly(pair.getRight()); + }; + + return DynamicTest.stream(list.iterator(), pair -> "search for: " + pair.getLeft(), test); + } + + @Test + void should_ReturnEmptyResults_ifInvalidCharacterIsUsedInCondition() { + String url = + restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=ksc-teamleads,cn=groups"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()).isNotNull().isEmpty(); + } + + @Test + void testGetMatches() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=rig"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getName) + .containsExactlyInAnyOrder("Schläfrig, Tim", "Eifrig, Elena"); + } + + @Test + void should_ReturnAccessIdWithUmlauten_ifBased64EncodedUserIsLookedUp() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=läf"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getName) + .containsExactlyInAnyOrder("Schläfrig, Tim"); + } + + @Test + void should_ThrowException_When_SearchForIsTooShort() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=al"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ThrowingCallable httpCall = + () -> { + TEMPLATE.exchange( + url, HttpMethod.GET, auth, ParameterizedTypeReference.forType(List.class)); + }; + + assertThatThrownBy(httpCall) + .isInstanceOf(HttpStatusCodeException.class) + .hasMessageContaining("Minimum Length is") + .extracting(HttpStatusCodeException.class::cast) + .extracting(HttpStatusCodeException::getStatusCode) + .isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void should_ReturnAccessIdsOfGroupsTheAccessIdIsMemberOf_ifAccessIdOfUserIsGiven() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + "?access-id=teamlead-2"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .usingElementComparator(String.CASE_INSENSITIVE_ORDER) + .containsExactlyInAnyOrder( + "ksc-teamleads", + "business-admins", + "monitor-users", + "Organisationseinheit KSC 2"); + } + + @Test + void should_ReturnAccessIdsOfPermissionsTheAccessIdIsMemberOf_ifAccessIdOfUserIsGiven() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_PERMISSIONS) + + "?access-id=user-1-2"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .usingElementComparator(String.CASE_INSENSITIVE_ORDER) + .containsExactlyInAnyOrder("taskana:callcenter:ab:ab/a:callcenter", + "taskana:callcenter:ab:ab/a:callcenter-vip"); + } + + @Test + void should_ValidateAccessIdWithEqualsFilterAndReturnAccessIdsOfGroupsTheAccessIdIsMemberOf() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + "?access-id=user-2-1"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .usingElementComparator(String.CASE_INSENSITIVE_ORDER) + .containsExactlyInAnyOrder( + "ksc-users", + "Organisationseinheit KSC 2"); + } + + @Test + void should_ValidateAccessIdWithEqualsFilterAndReturnAccessIdsOfPermissionsAccessIdIsMemberOf() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_PERMISSIONS) + "?access-id=user-2-1"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .usingElementComparator(String.CASE_INSENSITIVE_ORDER) + .containsExactlyInAnyOrder( + "taskana:callcenter:ab:ab/a:callcenter"); + } + + @Test + void should_ReturnBadRequest_ifAccessIdOfUserContainsInvalidCharacter() { + String url = + restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + "?access-id=teamlead-2,cn=users"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThatThrownBy(call) + .isInstanceOf(HttpStatusCodeException.class) + .hasMessageContaining("The AccessId is invalid") + .extracting(HttpStatusCodeException.class::cast) + .extracting(HttpStatusCodeException::getStatusCode) + .isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void should_ReturnAccessIdsOfGroupsTheAccessIdIsMemberOf_ifAccessIdOfGroupIsGiven() { + String url = + restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + + "?access-id=cn=Organisationseinheit KSC 1," + + "cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .usingElementComparator(String.CASE_INSENSITIVE_ORDER) + .containsExactlyInAnyOrder("Organisationseinheit KSC"); + } + + @Test + void should_ThrowNotAuthorizedException_ifCallerOfGroupRetrievalIsNotAdminOrBusinessAdmin() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + "?access-id=teamlead-2"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("user-1-1")); + + ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThatThrownBy(call) + .isInstanceOf(HttpStatusCodeException.class) + .extracting(HttpStatusCodeException.class::cast) + .extracting(HttpStatusCodeException::getStatusCode) + .isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void should_ThrowNotAuthorizedException_ifCallerOfPermissionRetrievalIsNotAdminOrBusinessAdmin() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_PERMISSIONS) + + "?access-id=teamlead-2"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("user-1-1")); + + ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThatThrownBy(call) + .isInstanceOf(HttpStatusCodeException.class) + .extracting(HttpStatusCodeException.class::cast) + .extracting(HttpStatusCodeException::getStatusCode) + .isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void should_ThrowNotAuthorizedException_ifCallerOfValidationIsNotAdminOrBusinessAdmin() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=al"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("user-1-1")); + + ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThatThrownBy(call) + .isInstanceOf(HttpStatusCodeException.class) + .extracting(HttpStatusCodeException.class::cast) + .extracting(HttpStatusCodeException::getStatusCode) + .isEqualTo(HttpStatus.FORBIDDEN); + } +} diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerIntTest.java index f39aea9a29..bcac3368d2 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerIntTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/AccessIdControllerIntTest.java @@ -46,7 +46,11 @@ Stream should_ResolveAccessId_When_SearchingForDnOrCn() { Pair.of("uid=teamlead-1,cn=users,OU=Test,O=TASKANA", "teamlead-1"), Pair.of("ksc-use", "cn=ksc-users,cn=groups,ou=test,o=taskana"), Pair.of("user-b-2", "user-b-2"), - Pair.of("User-b-2", "user-b-2")); + Pair.of("User-b-2", "user-b-2"), + Pair.of("cn=g01,cn=groups,OU=Test,O=TASKANA", + "taskana:callcenter:ab:ab/a:callcenter"), + Pair.of("cn=g02,cn=groups,OU=Test,O=TASKANA", + "taskana:callcenter:ab:ab/a:callcenter-vip")); ThrowingConsumer> test = pair -> { @@ -146,6 +150,23 @@ void should_ReturnAccessIdsOfGroupsTheAccessIdIsMemberOf_ifAccessIdOfUserIsGiven + "cn=Organisationseinheit KSC,cn=organisation,OU=Test,O=TASKANA"); } + @Test + void should_ReturnAccessIdsOfPermissionsTheAccessIdIsMemberOf_ifAccessIdOfUserIsGiven() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_PERMISSIONS) + + "?access-id=user-1-2"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .usingElementComparator(String.CASE_INSENSITIVE_ORDER) + .containsExactlyInAnyOrder("taskana:callcenter:ab:ab/a:callcenter", + "taskana:callcenter:ab:ab/a:callcenter-vip"); + } + @Test void should_ValidateAccessIdWithEqualsFilterAndReturnAccessIdsOfGroupsTheAccessIdIsMemberOf() { String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_GROUPS) + "?access-id=user-2-1"; @@ -164,6 +185,22 @@ void should_ValidateAccessIdWithEqualsFilterAndReturnAccessIdsOfGroupsTheAccessI + "cn=organisation,ou=Test,O=TASKANA"); } + @Test + void should_ValidateAccessIdWithEqualsFilterAndReturnAccessIdsOfPermissionsAccessIdIsMemberOf() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_PERMISSIONS) + "?access-id=user-2-1"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("teamlead-1")); + + ResponseEntity> response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThat(response.getBody()) + .isNotNull() + .extracting(AccessIdRepresentationModel::getAccessId) + .usingElementComparator(String.CASE_INSENSITIVE_ORDER) + .containsExactlyInAnyOrder( + "taskana:callcenter:ab:ab/a:callcenter"); + } + @Test void should_ReturnBadRequest_ifAccessIdOfUserContainsInvalidCharacter() { String url = @@ -212,6 +249,21 @@ void should_ThrowNotAuthorizedException_ifCallerOfGroupRetrievalIsNotAdminOrBusi .isEqualTo(HttpStatus.FORBIDDEN); } + @Test + void should_ThrowNotAuthorizedException_ifCallerOfPermissionRetrievalIsNotAdminOrBusinessAdmin() { + String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID_PERMISSIONS) + + "?access-id=teamlead-2"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("user-1-1")); + + ThrowingCallable call = () -> TEMPLATE.exchange(url, HttpMethod.GET, auth, ACCESS_ID_LIST_TYPE); + + assertThatThrownBy(call) + .isInstanceOf(HttpStatusCodeException.class) + .extracting(HttpStatusCodeException.class::cast) + .extracting(HttpStatusCodeException::getStatusCode) + .isEqualTo(HttpStatus.FORBIDDEN); + } + @Test void should_ThrowNotAuthorizedException_ifCallerOfValidationIsNotAdminOrBusinessAdmin() { String url = restHelper.toUrl(RestEndpoints.URL_ACCESS_ID) + "?search-for=al"; diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/TaskanaEngineControllerIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/TaskanaEngineControllerIntTest.java index 1bde6a4e92..5380c7bb7d 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/TaskanaEngineControllerIntTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/TaskanaEngineControllerIntTest.java @@ -7,7 +7,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import pro.taskana.common.api.TaskanaRole; @@ -85,6 +87,25 @@ void should_ReturnOnlyClassificationsForTypeDocument_When_GetClassificationCateg assertThat(response.getBody()).containsExactly("EXTERNAL"); } + @Test + void should_ReturnUserInformation_WhenCurrentUserNotAuthorizedToUseUserFromHeader() { + HttpHeaders headers = RestHelper.generateHeadersForUser("user-2-1"); + headers.add("userid", "user-1-1"); + ResponseEntity response = + TEMPLATE.exchange( + restHelper.toUrl(RestEndpoints.URL_CURRENT_USER), + HttpMethod.GET, + new HttpEntity<>(headers), + ParameterizedTypeReference.forType(TaskanaUserInfoRepresentationModel.class)); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + TaskanaUserInfoRepresentationModel currentUser = response.getBody(); + assertThat(currentUser).isNotNull(); + assertThat(currentUser.getUserId()).isEqualTo("user-2-1"); + assertThat(currentUser.getGroupIds()).hasSize(3); + assertThat(currentUser.getRoles()).hasSize(1); + } + @Test void testGetCurrentUserInfo() { String url = restHelper.toUrl(RestEndpoints.URL_CURRENT_USER); @@ -105,6 +126,29 @@ void testGetCurrentUserInfo() { .doesNotContain(TaskanaRole.ADMIN); } + @Test + void testGetCurrentUserInfoWithPermission() { + String url = restHelper.toUrl(RestEndpoints.URL_CURRENT_USER); + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("user-1-2")); + + ResponseEntity response = + TEMPLATE.exchange( + url, + HttpMethod.GET, + auth, + ParameterizedTypeReference.forType(TaskanaUserInfoRepresentationModel.class)); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getUserId()).isEqualTo("user-1-2"); + assertThat(response.getBody().getGroupIds()) + .containsExactlyInAnyOrder("cn=organisationseinheit ksc 1,cn=organisationseinheit " + + "ksc,cn=organisation,ou=test,o=taskana", + "cn=ksc-users,cn=groups,ou=test,o=taskana", "cn=g02,cn=groups,ou=test,o=taskana", + "cn=g01,cn=groups,ou=test,o=taskana"); + assertThat(response.getBody().getRoles()) + .contains(TaskanaRole.USER) + .doesNotContain(TaskanaRole.ADMIN); + } + @Test void should_ReturnCustomAttributes() { String url = restHelper.toUrl(RestEndpoints.URL_CUSTOM_ATTRIBUTES); diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/ldap/LdapClientTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/ldap/LdapClientTest.java index d6172886cf..b7e76c9682 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/ldap/LdapClientTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/common/rest/ldap/LdapClientTest.java @@ -17,11 +17,14 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; @@ -45,37 +48,39 @@ class LdapClientTest { @Spy @InjectMocks LdapClient cut; - @Test - void should_SearchGroupByDn_For_LdapCall() { - setUpEnvMock(); - cut.init(); - - cut.searchAccessIdByDn("cn=developersgroup,ou=groups,o=taskanatest"); - - verify(ldapTemplate) - .lookup(eq("cn=developersgroup,ou=groups"), any(), any(LdapClient.DnContextMapper.class)); - } - - @Test - void should_ConvertAccessIdToLowercase_When_SearchingGroupByDn() { + @ParameterizedTest + @CsvSource(value = { + "cn=developersgroup,ou=groups,o=taskanatest;cn=developersgroup,ou=groups", + "cn=developers:permission,cn=groups;" + + "cn=developers:permission,cn=groups", + "cn=Developersgroup,ou=groups,o=taskanatest;cn=developersgroup,ou=groups", + "cn=Developers:Permission,cn=groups,o=taskanatest;" + + "cn=developers:permission,cn=groups" + }, delimiter = ';') + void should_SearchGroupOrPermissionByDnAndConvertAccessIdToLowercase_For_LdapCall(String arg1, + String arg2) throws InvalidNameException { setUpEnvMock(); cut.init(); - cut.searchAccessIdByDn("cn=Developersgroup,ou=groups,o=taskanatest"); + cut.searchAccessIdByDn(arg1); verify(ldapTemplate) - .lookup(eq("cn=developersgroup,ou=groups"), any(), any(LdapClient.DnContextMapper.class)); + .lookup(eq(new LdapName(arg2)), any(), any(LdapClient.DnContextMapper.class)); } @Test - void testLdap_searchUsersAndGroups() throws Exception { + void testLdap_searchUsersAndGroupsAndPermissions() throws Exception { setUpEnvMock(); cut.init(); + AccessIdRepresentationModel permission = new AccessIdRepresentationModel("testP", "testPId"); AccessIdRepresentationModel group = new AccessIdRepresentationModel("testG", "testGId"); AccessIdRepresentationModel user = new AccessIdRepresentationModel("testU", "testUId"); + when(ldapTemplate.search( + any(String.class), any(), anyInt(), any(), any(LdapClient.PermissionContextMapper.class))) + .thenReturn(List.of(permission)); when(ldapTemplate.search( any(String.class), any(), anyInt(), any(), any(LdapClient.GroupContextMapper.class))) .thenReturn(List.of(group)); @@ -83,7 +88,9 @@ void testLdap_searchUsersAndGroups() throws Exception { any(String.class), any(), anyInt(), any(), any(LdapClient.UserContextMapper.class))) .thenReturn(List.of(user)); - assertThat(cut.searchUsersAndGroups("test")).hasSize(2).containsExactlyInAnyOrder(user, group); + assertThat(cut.searchUsersAndGroupsAndPermissions("test")) + .hasSize(3) + .containsExactlyInAnyOrder(user, group, permission); } @Test @@ -105,19 +112,21 @@ void should_CorrectlySortAccessIds_When_ContainingNullAccessId() { } @Test - void should_ReturnAllUsersAndMembersOfGroupsWithTaskanaUserRole() throws Exception { + void should_ReturnAllUsersAndMembersOfGroupsAndMemberOfPermissionsWithTaskanaUserRole() { setUpEnvMock(); cut.init(); - AccessIdRepresentationModel user = new AccessIdRepresentationModel("testU", "testUId"); - Set groupsOfUserRole = new HashSet<>(); Map> roleMap = new HashMap<>(); roleMap.put(TaskanaRole.USER, groupsOfUserRole); + Set permissionsOfUserRole = new HashSet<>(); + roleMap.put(TaskanaRole.USER, permissionsOfUserRole); when(taskanaConfiguration.getRoleMap()).thenReturn(roleMap); + AccessIdRepresentationModel user = new AccessIdRepresentationModel("testU", "testUId"); + when(ldapTemplate.search( any(String.class), any(), anyInt(), any(), any(LdapClient.UserContextMapper.class))) .thenReturn(List.of(user)); @@ -126,7 +135,7 @@ void should_ReturnAllUsersAndMembersOfGroupsWithTaskanaUserRole() throws Excepti } @Test - void testLdap_getNameWithoutBaseDn() { + void testLdap_getNameWithoutBaseDnForGroup() { setUpEnvMock(); cut.init(); @@ -135,11 +144,23 @@ void testLdap_getNameWithoutBaseDn() { } @Test - void shouldNot_CreateOrCriteriaWithDnAndAccessIdString_When_PropertyTypeIsSet() - throws InvalidArgumentException { + void testLdap_getNameWithoutBaseDnForPermission() { setUpEnvMock(); - lenient().when(this.environment.getProperty("taskana.ldap.groupsOfUser.type")).thenReturn("dn"); + cut.init(); + assertThat(cut.getNameWithoutBaseDn("cn=other:permission,cn=groups,o=taskanatest")) + .isEqualTo("cn=other:permission,cn=groups"); + } + + @Test + void shouldNot_CreateOrCriteriaWithDnAndAccessIdStringForGroup_When_PropertyTypeIsSet() + throws InvalidArgumentException, InvalidNameException { + + setUpEnvMock(); + lenient().when(this.environment.getProperty("taskana.ldap.groupsOfUser.type")) + .thenReturn("dn"); + lenient().when(this.environment.getProperty("taskana.ldap.permissionsOfUser.type")) + .thenReturn("dn"); lenient() .when( ldapTemplate.search( @@ -153,9 +174,10 @@ void shouldNot_CreateOrCriteriaWithDnAndAccessIdString_When_PropertyTypeIsSet() cut.init(); cut.searchGroupsAccessIdIsMemberOf("user-1-1"); + cut.searchPermissionsAccessIdHas("user-1-1"); - String expectedFilterValue = - "(&(objectclass=groupOfUniqueNames)(memberUid=uid=user-1-1,cn=users,OU=Test,O=TASKANA))"; + String expectedFilterValue = "(&(!(permission=*))" + + "(&(objectclass=groupOfUniqueNames)(memberUid=uid=user-1-1,cn=users,OU=Test,O=TASKANA)))"; verify(ldapTemplate) .search( any(String.class), @@ -163,6 +185,16 @@ void shouldNot_CreateOrCriteriaWithDnAndAccessIdString_When_PropertyTypeIsSet() anyInt(), any(), any(LdapClient.GroupContextMapper.class)); + + String expectedFilterValueForPermission = "(&(permission=*)" + + "(&(objectclass=groupOfUniqueNames)(memberUid=uid=user-1-1,cn=users,OU=Test,O=TASKANA)))"; + verify(ldapTemplate) + .search( + any(String.class), + eq(expectedFilterValueForPermission), + anyInt(), + any(), + any(LdapClient.PermissionContextMapper.class)); } @Test @@ -173,7 +205,7 @@ void testLdap_getFirstPageOfaResultList() { List result = IntStream.range(0, 100) .mapToObj(i -> new AccessIdRepresentationModel("" + i, "" + i)) - .collect(Collectors.toList()); + .toList(); assertThat(cut.getFirstPageOfaResultList(result)) .hasSize(cut.getMaxNumberOfReturnedAccessIds()); @@ -193,7 +225,7 @@ void testLdap_checkForMissingConfigurations() { // userMobilePhoneAttribute, userEmailAttribute, userOrglevel1Attribute, userOrglevel2Attribute, // userOrglevel3Attribute, userOrglevel4Attribute, groupsOfUser, groupsOfUserName, // groupOfUserType - assertThat(cut.checkForMissingConfigurations()).hasSize(LdapSettings.values().length - 12); + assertThat(cut.checkForMissingConfigurations()).hasSize(LdapSettings.values().length - 15); } @Test @@ -231,7 +263,14 @@ private void setUpEnvMock() { {"taskana.ldap.userOrglevel1Attribute", "orgLevel1"}, {"taskana.ldap.userOrglevel2Attribute", "orgLevel2"}, {"taskana.ldap.userOrglevel3Attribute", "orgLevel3"}, - {"taskana.ldap.userOrglevel4Attribute", "orgLevel4"} + {"taskana.ldap.userOrglevel4Attribute", "orgLevel4"}, + {"taskana.ldap.permissionsOfUser", "memberUid"}, + {"taskana.ldap.permissionNameAttribute", "permission"}, + {"taskana.ldap.permissionSearchFilterValue", "groupOfUniqueNames"}, + {"taskana.ldap.permissionSearchFilterName", "objectclass"}, + {"taskana.ldap.permissionSearchBase", "ou=groups"}, + {"taskana.ldap.userPermissionsAttribute", "permission"}, + {"taskana.ldap.useDnForGroups", "false"}, }) .forEach( strings -> diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/workbasket/rest/WorkbasketControllerIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/workbasket/rest/WorkbasketControllerIntTest.java index d9ff6c1ee6..664d30c9f7 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/workbasket/rest/WorkbasketControllerIntTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/workbasket/rest/WorkbasketControllerIntTest.java @@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static pro.taskana.rest.test.RestHelper.TEMPLATE; -import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.Instant; @@ -42,7 +41,7 @@ class WorkbasketControllerIntTest { } @Test - void testGetWorkbasket() throws UnsupportedEncodingException { + void testGetWorkbasket() { final String url = restHelper.toUrl( RestEndpoints.URL_WORKBASKET_ID, "WBI%3A100000000000000000000000000000000006"); @@ -278,7 +277,7 @@ void testGetWorkbasketAccessItems() { assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().getLink(IanaLinkRelations.SELF)).isNotNull(); assertThat(response.getHeaders().getContentType()).isEqualTo(MediaTypes.HAL_JSON); - assertThat(response.getBody().getContent()).hasSize(3); + assertThat(response.getBody().getContent()).hasSize(4); } @Test diff --git a/rest/taskana-rest-spring/src/test/resources/application.properties b/rest/taskana-rest-spring/src/test/resources/application.properties index 19c3098ba7..8c1511d2ac 100644 --- a/rest/taskana-rest-spring/src/test/resources/application.properties +++ b/rest/taskana-rest-spring/src/test/resources/application.properties @@ -37,6 +37,13 @@ taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 taskana.ldap.groupsOfUser=uniquemember taskana.ldap.groupsOfUser.type= +taskana.ldap.permissionSearchBase= +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true + # Embedded Spring LDAP server spring.ldap.embedded.base-dn=OU=Test,O=TASKANA spring.ldap.embedded.credential.username=uid=admin diff --git a/routing/taskana-routing-rest/src/test/resources/application.properties b/routing/taskana-routing-rest/src/test/resources/application.properties index f9a08ea66a..3b5e15a2b2 100644 --- a/routing/taskana-routing-rest/src/test/resources/application.properties +++ b/routing/taskana-routing-rest/src/test/resources/application.properties @@ -42,6 +42,12 @@ taskana.ldap.groupNameAttribute=cn taskana.ldap.minSearchForLength=3 taskana.ldap.maxNumberOfReturnedAccessIds=50 taskana.ldap.groupsOfUser=uniquemember +taskana.ldap.permissionSearchBase= +taskana.ldap.permissionSearchFilterName=objectclass +taskana.ldap.permissionSearchFilterValue=groupofuniquenames +taskana.ldap.permissionNameAttribute=permission +taskana.ldap.permissionsOfUser=uniquemember +taskana.ldap.useDnForGroups=true # Embedded Spring LDAP server spring.ldap.embedded.base-dn= OU=Test,O=TASKANA spring.ldap.embedded.credential.username= uid=admin diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.html b/web/src/app/administration/components/access-items-management/access-items-management.component.html index 4ea122bfb5..2e7094bacb 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.html +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.html @@ -34,6 +34,29 @@

Select an access id

The user is not associated to any groups + + + + + Permissions of {{accessId.accessId}} + + + + + + + + + + + + + +
Name {{element.name}} Access Id {{element.accessId}}
+ + The user is not associated to any permissions +
+ diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.scss b/web/src/app/administration/components/access-items-management/access-items-management.component.scss index f564cd8860..b412985186 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.scss +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.scss @@ -45,6 +45,11 @@ tr:first-child > td { text-align: left; } + &__permission-table-cell { + height: 48px; + text-align: left; + } + &__table { overflow-x: auto; overflow-y: hidden; @@ -67,6 +72,11 @@ tr:first-child > td { left: 1%; } + &__permissions-expansion-panel { + width: 98%; + left: 1%; + } + &__authorization-expansion-panel { width: 98%; left: 1%; diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts b/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts index e02cdcabc4..ac1ce973c1 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts @@ -146,6 +146,9 @@ describe('AccessItemsManagementComponent', () => { const groups = store.selectSnapshot((state) => state.accessItemsManagement); expect(groups).toBeDefined(); + + const permissions = store.selectSnapshot((state) => state.accessItemsManagement); + expect(permissions).toBeDefined(); }); it('should be able to get groups if selected access ID is not null in onSelectAccessId', () => { @@ -158,12 +161,30 @@ describe('AccessItemsManagementComponent', () => { expect(groups).toMatchObject({}); }); + it('should be able to get permissions if selected access ID is not null in onSelectAccessId', () => { + const selectedAccessId = { accessId: '1', name: '' }; + app.permissions = [ + { accessId: '1', name: 'perm' }, + { accessId: '2', name: 'perm' } + ]; + app.onSelectAccessId(selectedAccessId); + const permissions = store.selectSnapshot((state) => state.accessItemsManagement); + expect(selectedAccessId).not.toBeNull(); + expect(permissions).not.toBeNull(); + app.onSelectAccessId(null); + expect(permissions).toMatchObject({}); + }); + it('should dispatch GetAccessItems action in searchForAccessItemsWorkbaskets', async((done) => { app.accessId = { accessId: '1', name: 'max' }; app.groups = [ { accessId: '1', name: 'users' }, { accessId: '2', name: 'users' } ]; + app.permissions = [ + { accessId: '1', name: 'perm' }, + { accessId: '2', name: 'perm' } + ]; app.sortModel = { 'sort-by': WorkbasketAccessItemQuerySortParameter.ACCESS_ID, order: Direction.DESC @@ -175,6 +196,7 @@ describe('AccessItemsManagementComponent', () => { actionDispatched = true; expect(actionDispatched).toBe(true); expect(app.setAccessItemsGroups).toHaveBeenCalled(); + expect(app.setAccessItemsPermissions).toHaveBeenCalled(); done(); }); })); @@ -194,6 +216,12 @@ describe('AccessItemsManagementComponent', () => { expect(app.accessItemsForm).not.toBeNull(); }); + it('should create accessItemsForm in setAccessItemsPermissions', () => { + app.setAccessItemsPermissions([]); + expect(app.accessItemsForm).toBeDefined(); + expect(app.accessItemsForm).not.toBeNull(); + }); + it('should invoke sorting function correctly', () => { const newSort: Sorting = { 'sort-by': WorkbasketAccessItemQuerySortParameter.ACCESS_ID, @@ -201,6 +229,7 @@ describe('AccessItemsManagementComponent', () => { }; app.accessId = { accessId: '1', name: 'max' }; app.groups = [{ accessId: '1', name: 'users' }]; + app.permissions = [{ accessId: '1', name: 'perm' }]; app.sorting(newSort); expect(app.sortModel).toMatchObject(newSort); }); @@ -209,4 +238,9 @@ describe('AccessItemsManagementComponent', () => { app.accessItemsForm = null; expect(app.accessItemsGroups).toBeNull(); }); + + it('should not return accessItemsPermissions when accessItemsForm is null', () => { + app.accessItemsForm = null; + expect(app.accessItemsPermissions).toBeNull(); + }); }); diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.ts b/web/src/app/administration/components/access-items-management/access-items-management.component.ts index 1c920fa5cc..4fdaf8a193 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.ts +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.ts @@ -18,6 +18,7 @@ import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../.. import { customFieldCount } from '../../../shared/models/workbasket-access-items'; import { GetAccessItems, + GetPermissionsByAccessId, GetGroupsByAccessId, RemoveAccessItemsPermissions } from '../../../shared/store/access-items-management-store/access-items-management.actions'; @@ -38,6 +39,7 @@ export class AccessItemsManagementComponent implements OnInit { accessItemsForm: FormGroup; accessId: AccessId; groups: AccessId[]; + permissions: AccessId[]; defaultSortBy: WorkbasketAccessItemQuerySortParameter = WorkbasketAccessItemQuerySortParameter.ACCESS_ID; sortingFields: Map = WORKBASKET_ACCESS_ITEM_SORT_PARAMETER_NAMING; sortModel: Sorting = { @@ -46,11 +48,13 @@ export class AccessItemsManagementComponent implements OnInit { }; accessItems: WorkbasketAccessItems[]; isGroup: boolean = false; + isPermission: boolean = false; @Select(EngineConfigurationSelectors.accessItemsCustomisation) accessItemsCustomization$: Observable; @Select(AccessItemsManagementSelector.groups) groups$: Observable; customFields$: Observable; + @Select(AccessItemsManagementSelector.permissions) permissions$: Observable; destroy$ = new Subject(); constructor( @@ -65,6 +69,9 @@ export class AccessItemsManagementComponent implements OnInit { this.groups$.pipe(takeUntil(this.destroy$)).subscribe((groups) => { this.groups = groups; }); + this.permissions$.pipe(takeUntil(this.destroy$)).subscribe((permissions) => { + this.permissions = permissions; + }); } onSelectAccessId(selected: AccessId) { @@ -79,6 +86,12 @@ export class AccessItemsManagementComponent implements OnInit { .subscribe(() => { this.searchForAccessItemsWorkbaskets(); }); + this.store + .dispatch(new GetPermissionsByAccessId(selected.accessId)) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.searchForAccessItemsWorkbaskets(); + }); } } else { this.accessItemsForm = null; @@ -88,19 +101,40 @@ export class AccessItemsManagementComponent implements OnInit { searchForAccessItemsWorkbaskets() { this.removeFocus(); - const filterParameter: WorkbasketAccessItemQueryFilterParameter = { - 'access-id': [this.accessId, ...this.groups].map((a) => a.accessId) - }; - this.store - .dispatch(new GetAccessItems(filterParameter, this.sortModel)) - .pipe(takeUntil(this.destroy$)) - .subscribe((state) => { - this.setAccessItemsGroups( - state['accessItemsManagement'].accessItemsResource - ? state['accessItemsManagement'].accessItemsResource.accessItems - : [] - ); - }); + if (this.permissions == null) { + const filterParameter: WorkbasketAccessItemQueryFilterParameter = { + 'access-id': [this.accessId, ...this.groups].map((a) => a.accessId) + }; + this.store + .dispatch(new GetAccessItems(filterParameter, this.sortModel)) + .pipe(takeUntil(this.destroy$)) + .subscribe((state) => { + this.setAccessItemsGroups( + state['accessItemsManagement'].accessItemsResource + ? state['accessItemsManagement'].accessItemsResource.accessItems + : [] + ); + }); + } else { + const filterParameter: WorkbasketAccessItemQueryFilterParameter = { + 'access-id': [this.accessId, ...this.groups, ...this.permissions].map((a) => a.accessId) + }; + this.store + .dispatch(new GetAccessItems(filterParameter, this.sortModel)) + .pipe(takeUntil(this.destroy$)) + .subscribe((state) => { + this.setAccessItemsPermissions( + state['accessItemsManagement'].accessItemsResource + ? state['accessItemsManagement'].accessItemsResource.accessItems + : [] + ); + this.setAccessItemsGroups( + state['accessItemsManagement'].accessItemsResource + ? state['accessItemsManagement'].accessItemsResource.accessItems + : [] + ); + }); + } } setAccessItemsGroups(accessItems: Array) { @@ -129,6 +163,32 @@ export class AccessItemsManagementComponent implements OnInit { } } + setAccessItemsPermissions(accessItems: Array) { + const AccessItemsFormPermissions = accessItems.map((accessItem) => this.formBuilder.group(accessItem)); + AccessItemsFormPermissions.forEach((accessItemPermission) => { + accessItemPermission.controls.accessId.setValidators(Validators.required); + Object.keys(accessItemPermission.controls).forEach((key) => { + accessItemPermission.controls[key].disable(); + }); + }); + + const AccessItemsFormArray = this.formBuilder.array(AccessItemsFormPermissions); + if (!this.accessItemsForm) { + this.accessItemsForm = this.formBuilder.group({}); + } + this.accessItemsForm.setControl('accessItemsPermissions', AccessItemsFormArray); + if (!this.accessItemsForm.value.workbasketKeyFilter) { + this.accessItemsForm.addControl('workbasketKeyFilter', new FormControl()); + } + if (!this.accessItemsForm.value.accessIdFilter) { + this.accessItemsForm.addControl('accessIdFilter', new FormControl()); + } + this.accessItems = accessItems; + if (this.accessItemsForm.value.workbasketKeyFilter || this.accessItemsForm.value.accessIdFilter) { + this.filterAccessItems(); + } + } + filterAccessItems() { if (this.accessItemsForm.value.accessIdFilter) { this.accessItems = this.accessItems.filter((value) => @@ -161,8 +221,15 @@ export class AccessItemsManagementComponent implements OnInit { return this.accessItemsForm ? (this.accessItemsForm.get('accessItemsGroups') as FormArray) : null; } + get accessItemsPermissions(): FormArray { + return this.accessItemsForm ? (this.accessItemsForm.get('accessItemsPermissions') as FormArray) : null; + } + isFieldValid(field: string, index: number): boolean { - return this.formsValidatorService.isFieldValid(this.accessItemsGroups[index], field); + return ( + this.formsValidatorService.isFieldValid(this.accessItemsGroups[index], field) || + this.formsValidatorService.isFieldValid(this.accessItemsPermissions[index], field) + ); } sorting(sort: Sorting) { diff --git a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts index a844b2d3b8..f9d6523edf 100644 --- a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts +++ b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts @@ -157,7 +157,13 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest this.selectedWorkbasket$.pipe(take(1)).subscribe((workbasket) => { this.accessItemsRepresentation._links = { self: { href: workbasket._links.accessItems.href } }; this.setWorkbasketIdForCopy(workbasket.workbasketId); - this.onSave(); + this.formsValidatorService + .validateFormAccess(this.accessItemsGroups, this.toggleValidationAccessIdMap) + .then((value) => { + if (value) { + this.onSave(); + } + }); }); }); diff --git a/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts b/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts index c29f7cf2cd..1fd0097e1b 100644 --- a/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts +++ b/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts @@ -3,13 +3,17 @@ import { DebugElement } from '@angular/core'; import { TypeAheadComponent } from './type-ahead.component'; import { AccessIdsService } from '../../services/access-ids/access-ids.service'; import { of } from 'rxjs'; -import { NgxsModule } from '@ngxs/store'; +import { NgxsModule, Store } from '@ngxs/store'; +import { HttpClientModule } from '@angular/common/http'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { EngineConfigurationState } from '../../store/engine-configuration-store/engine-configuration.state'; +import { ClassificationCategoriesService } from '../../services/classification-categories/classification-categories.service'; +import { engineConfigurationMock } from '../../store/mock-data/mock-store'; const accessIdService: Partial = { searchForAccessId: jest.fn().mockReturnValue(of([{ accessId: 'user-g-1', name: 'Gerda' }])) @@ -19,23 +23,29 @@ describe('TypeAheadComponent with AccessId input', () => { let fixture: ComponentFixture; let debugElement: DebugElement; let component: TypeAheadComponent; + let store: Store; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - NgxsModule.forRoot([]), + NgxsModule.forRoot([EngineConfigurationState]), MatFormFieldModule, MatInputModule, MatAutocompleteModule, MatTooltipModule, NoopAnimationsModule, FormsModule, - ReactiveFormsModule + ReactiveFormsModule, + HttpClientModule ], declarations: [TypeAheadComponent], - providers: [{ provide: AccessIdsService, useValue: accessIdService }] + providers: [{ provide: AccessIdsService, useValue: accessIdService }, ClassificationCategoriesService] }).compileComponents(); + store = TestBed.inject(Store); + store.reset({ + engineConfiguration: engineConfigurationMock + }); fixture = TestBed.createComponent(TypeAheadComponent); debugElement = fixture.debugElement; component = fixture.componentInstance; @@ -53,7 +63,7 @@ describe('TypeAheadComponent with AccessId input', () => { input.dispatchEvent(new Event('input')); component.accessIdForm.get('accessId').updateValueAndValidity({ emitEvent: true }); - tick(); + tick(50); expect(component.name).toBe('Gerda'); })); @@ -63,7 +73,7 @@ describe('TypeAheadComponent with AccessId input', () => { component.accessIdForm.get('accessId').setValue('invalid-user'); component.accessIdForm.get('accessId').updateValueAndValidity({ emitEvent: true }); - tick(); + tick(50); fixture.detectChanges(); expect(emitSpy).toHaveBeenCalledWith(false); })); @@ -73,7 +83,7 @@ describe('TypeAheadComponent with AccessId input', () => { component.accessIdForm.get('accessId').setValue('user-g-1'); component.accessIdForm.get('accessId').updateValueAndValidity({ emitEvent: true }); - tick(); + tick(50); fixture.detectChanges(); expect(emitSpy).toHaveBeenCalledWith(true); })); diff --git a/web/src/app/shared/components/type-ahead/type-ahead.component.ts b/web/src/app/shared/components/type-ahead/type-ahead.component.ts index f3dcdae2c6..91510fcddb 100644 --- a/web/src/app/shared/components/type-ahead/type-ahead.component.ts +++ b/web/src/app/shared/components/type-ahead/type-ahead.component.ts @@ -1,12 +1,14 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { AccessIdsService } from '../../services/access-ids/access-ids.service'; -import { Observable, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, Observable, Subject } from 'rxjs'; import { FormControl, FormGroup } from '@angular/forms'; import { AccessId } from '../../models/access-id'; -import { take, takeUntil } from 'rxjs/operators'; +import { map, take, takeUntil } from 'rxjs/operators'; import { Select } from '@ngxs/store'; import { WorkbasketSelectors } from '../../store/workbasket-store/workbasket.selectors'; import { ButtonAction } from '../../../administration/models/button-action'; +import { EngineConfigurationSelectors } from '../../store/engine-configuration-store/engine-configuration.selectors'; +import { GlobalCustomisation } from '../../models/customisation'; @Component({ selector: 'taskana-shared-type-ahead', @@ -24,12 +26,16 @@ export class TypeAheadComponent implements OnInit, OnDestroy { @Output() accessIdEventEmitter = new EventEmitter(); @Output() isFormValid = new EventEmitter(); + @Select(EngineConfigurationSelectors.globalCustomisation) + globalCustomisation$: Observable; + @Select(WorkbasketSelectors.buttonAction) buttonAction$: Observable; name: string = ''; lastSavedAccessId: string = ''; filteredAccessIds: AccessId[] = []; + debounceTime: number = 750; destroy$ = new Subject(); accessIdForm = new FormGroup({ accessId: new FormControl('') @@ -57,14 +63,27 @@ export class TypeAheadComponent implements OnInit, OnDestroy { } }); - this.accessIdForm.controls['accessId'].valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => { - const value = this.accessIdForm.controls['accessId'].value; - if (value === '') { - this.handleEmptyAccessId(); - return; - } - this.searchForAccessId(value); - }); + this.globalCustomisation$ + .pipe( + take(1), + map((customisation) => customisation?.debounceTimeLookupField) + ) + .subscribe((debounceTime) => { + if (!!debounceTime) { + this.debounceTime = debounceTime; + } + }); + + this.accessIdForm.controls['accessId'].valueChanges + .pipe(debounceTime(this.debounceTime), distinctUntilChanged(), takeUntil(this.destroy$)) + .subscribe(() => { + const value = this.accessIdForm.controls['accessId'].value; + if (value === '') { + this.handleEmptyAccessId(); + return; + } + this.searchForAccessId(value); + }); this.setAccessIdFromInput(); } diff --git a/web/src/app/shared/models/customisation.ts b/web/src/app/shared/models/customisation.ts index 421c8a79ad..5d58991be2 100644 --- a/web/src/app/shared/models/customisation.ts +++ b/web/src/app/shared/models/customisation.ts @@ -6,6 +6,7 @@ export interface Customisation { } export interface CustomisationContent { + global?: GlobalCustomisation; workbaskets?: WorkbasketsCustomisation; classifications?: ClassificationsCustomisation; tasks?: TasksCustomisation; @@ -31,6 +32,10 @@ export interface WorkbasketsCustomisation { 'access-items'?: AccessItemsCustomisation; } +export interface GlobalCustomisation { + debounceTimeLookupField: number; +} + export type AccessItemsCustomisation = { accessId?: LookupField } & CustomFields; export interface CustomFields { diff --git a/web/src/app/shared/services/access-ids/access-ids.service.ts b/web/src/app/shared/services/access-ids/access-ids.service.ts index 5c96c03634..8d77f24f4f 100644 --- a/web/src/app/shared/services/access-ids/access-ids.service.ts +++ b/web/src/app/shared/services/access-ids/access-ids.service.ts @@ -34,6 +34,13 @@ export class AccessIdsService { return this.httpClient.get(`${this.url}/groups?access-id=${accessId}`); } + getPermissionsByAccessId(accessId: string): Observable { + if (!accessId || accessId.length < 3) { + return of([]); + } + return this.httpClient.get(`${this.url}/permissions?access-id=${accessId}`); + } + getAccessItems( filterParameter?: WorkbasketAccessItemQueryFilterParameter, sortParameter?: Sorting, diff --git a/web/src/app/shared/services/forms-validator/forms-validator.service.ts b/web/src/app/shared/services/forms-validator/forms-validator.service.ts index 280ef0764f..291aba1d40 100644 --- a/web/src/app/shared/services/forms-validator/forms-validator.service.ts +++ b/web/src/app/shared/services/forms-validator/forms-validator.service.ts @@ -90,7 +90,8 @@ export class FormsValidatorService { } form.controls.forEach((control) => { - const { permEditTasks, permReadTasks, permRead } = control.value; + const { permEditTasks, permReadTasks, permRead, permOpen, permDistribute, permAppend, permTransfer } = + control.value; if (permEditTasks && (!permReadTasks || !permRead)) { this.notificationsService.showWarning('PERM_EDIT_TASKS_MISSING_DEPENDING_PERMISSION'); @@ -99,6 +100,14 @@ export class FormsValidatorService { if (permReadTasks && !permRead) { this.notificationsService.showWarning('PERM_READ_TASKS_MISSING_DEPENDING_PERMISSIONS'); } + + if (permOpen && (!permReadTasks || !permRead)) { + this.notificationsService.showWarning('PERM_OPEN_MISSING_DEPENDING_PERMISSIONS'); + } + + if (permDistribute && (!permAppend || !permTransfer)) { + this.notificationsService.showWarning('PERM_DISTRIBUTE_MISSING_DEPENDING_PERMISSIONS'); + } }); return result; diff --git a/web/src/app/shared/services/obtain-message/message-by-error-code.ts b/web/src/app/shared/services/obtain-message/message-by-error-code.ts index ce4dd4b9ca..dc1b3a35b8 100644 --- a/web/src/app/shared/services/obtain-message/message-by-error-code.ts +++ b/web/src/app/shared/services/obtain-message/message-by-error-code.ts @@ -109,6 +109,12 @@ export const messageByErrorCode = { PERM_READ_TASKS_MISSING_DEPENDING_PERMISSIONS: '"Read tasks" permission was selected without the required "Read" permission. ' + 'Your changes will still be saved but they might lead to unexpected behavior.', + PERM_OPEN_MISSING_DEPENDING_PERMISSIONS: + '"Open" permission was selected without the required "Read tasks" and "Read" permissions. ' + + 'Your changes will still be saved but they might lead to unexpected behavior.', + PERM_DISTRIBUTE_MISSING_DEPENDING_PERMISSIONS: + '"Distribute" permission was selected without the required "Append" and "Transfer" permissions. ' + + 'Your changes will still be saved but they might lead to unexpected behavior.', REPORT_DATA_WRONG_HEADER: 'The received header of the Report data does not match the expected header. ' + 'The data might be displayed incorrectly. Please contact your administrator.', diff --git a/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts b/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts index ff367ae92b..5c08970de2 100644 --- a/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts +++ b/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts @@ -13,6 +13,11 @@ export class GetGroupsByAccessId { constructor(public accessId: string) {} } +export class GetPermissionsByAccessId { + static readonly type = '[Access Items Management] Get permissions by access ID'; + constructor(public accessId: string) {} +} + export class GetAccessItems { static readonly type = '[Access Items Management] Get access items'; constructor( diff --git a/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts b/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts index 6d87d727c9..8759d8203d 100644 --- a/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts +++ b/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts @@ -7,4 +7,9 @@ export class AccessItemsManagementSelector { static groups(state: AccessItemsManagementStateModel): AccessId[] { return state.groups; } + + @Selector([AccessItemsManagementState]) + static permissions(state: AccessItemsManagementStateModel): AccessId[] { + return state.permissions; + } } diff --git a/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts b/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts index 300b12d1a5..409e32aaba 100644 --- a/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts +++ b/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts @@ -1,6 +1,7 @@ import { Action, NgxsAfterBootstrap, State, StateContext } from '@ngxs/store'; import { GetAccessItems, + GetPermissionsByAccessId, GetGroupsByAccessId, RemoveAccessItemsPermissions, SelectAccessId @@ -56,6 +57,26 @@ export class AccessItemsManagementState implements NgxsAfterBootstrap { ); } + @Action(GetPermissionsByAccessId) + getPermissionsByAccessId( + ctx: StateContext, + action: GetPermissionsByAccessId + ): Observable { + return this.accessIdsService.getPermissionsByAccessId(action.accessId).pipe( + take(1), + tap( + (permissions: AccessId[]) => { + ctx.patchState({ + permissions + }); + }, + () => { + this.requestInProgressService.setRequestInProgress(false); + } + ) + ); + } + @Action(GetAccessItems) getAccessItems(ctx: StateContext, action: GetAccessItems): Observable { this.requestInProgressService.setRequestInProgress(true); @@ -108,4 +129,5 @@ export interface AccessItemsManagementStateModel { accessItemsResource: WorkbasketAccessItemsRepresentation; selectedAccessId: AccessId; groups: AccessId[]; + permissions: AccessId[]; } diff --git a/web/src/app/shared/store/engine-configuration-store/engine-configuration.selectors.ts b/web/src/app/shared/store/engine-configuration-store/engine-configuration.selectors.ts index b3ef8130f0..3f016a281c 100644 --- a/web/src/app/shared/store/engine-configuration-store/engine-configuration.selectors.ts +++ b/web/src/app/shared/store/engine-configuration-store/engine-configuration.selectors.ts @@ -3,12 +3,18 @@ import { ClassificationsCustomisation, AccessItemsCustomisation, TasksCustomisation, - ClassificationCategoryImages + ClassificationCategoryImages, + GlobalCustomisation } from 'app/shared/models/customisation'; import { Selector } from '@ngxs/store'; import { EngineConfigurationStateModel, EngineConfigurationState } from './engine-configuration.state'; export class EngineConfigurationSelectors { + @Selector([EngineConfigurationState]) + static globalCustomisation(state: EngineConfigurationStateModel): GlobalCustomisation { + return state.customisation[state.language].global; + } + @Selector([EngineConfigurationState]) static workbasketsCustomisation(state: EngineConfigurationStateModel): WorkbasketsCustomisation { return state.customisation[state.language].workbaskets; diff --git a/web/src/app/shared/store/mock-data/mock-store.ts b/web/src/app/shared/store/mock-data/mock-store.ts index e8253e2afe..4c17409c21 100644 --- a/web/src/app/shared/store/mock-data/mock-store.ts +++ b/web/src/app/shared/store/mock-data/mock-store.ts @@ -17,6 +17,9 @@ export const classificationStateMock = { export const engineConfigurationMock = { customisation: { EN: { + global: { + debounceTimeLookupField: 50 + }, workbaskets: { information: { owner: { diff --git a/web/src/environments/data-sources/taskana-customization.json b/web/src/environments/data-sources/taskana-customization.json index d89fb7158f..4782731958 100644 --- a/web/src/environments/data-sources/taskana-customization.json +++ b/web/src/environments/data-sources/taskana-customization.json @@ -1,5 +1,8 @@ { "EN": { + "global": { + "debounceTimeLookupField": 750 + }, "workbaskets": { "information": { "owner": {