From 6361d3bb41f8eef49b809267de632b9e0b6fa69e Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Wed, 13 May 2026 08:42:18 +0200 Subject: [PATCH 1/5] [NAE-2424] UserRefs negative view permissions aren't resolved - negativeViewUsers resolution fix in Case and Task - added test TaskPermissionsTest with test nets view_permission_combinations.xml and view_permission_combinations_no_default.xml --- .../engine/workflow/domain/Case.java | 9 +- .../engine/workflow/domain/Task.java | 9 +- .../engine/workflow/service/TaskService.java | 5 +- .../workflow/service/WorkflowService.java | 9 +- .../workflow/TaskPermissionsTest.groovy | 138 +++++ .../view_permission_combinations.xml | 532 ++++++++++++++++++ ...iew_permission_combinations_no_default.xml | 154 +++++ 7 files changed, 843 insertions(+), 13 deletions(-) create mode 100644 src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy create mode 100644 src/test/resources/petriNets/view_permission_combinations.xml create mode 100644 src/test/resources/petriNets/view_permission_combinations_no_default.xml diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java index 1559cbe001..9268326550 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java @@ -350,9 +350,16 @@ public void resolveViewUserRefs() { public void resolveViewUsers() { getViewUsers(); this.viewUsers.clear(); + this.negativeViewUsers.clear(); this.users.forEach((user, perms) -> { - if (perms.containsKey(RolePermission.VIEW.getValue()) && perms.get(RolePermission.VIEW.getValue())) { + if (!perms.containsKey(RolePermission.VIEW.getValue())) { + return; + } + boolean viewPermission = perms.get(RolePermission.VIEW.getValue()); + if(viewPermission){ viewUsers.add(user); + } else { + negativeViewUsers.add(user); } }); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/Task.java b/src/main/java/com/netgrif/application/engine/workflow/domain/Task.java index 75d7e999c4..795059a595 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/Task.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/Task.java @@ -329,9 +329,16 @@ public void resolveViewUserRefs() { public void resolveViewUsers() { getViewUsers(); this.viewUsers.clear(); + this.negativeViewUsers.clear(); this.users.forEach((role, perms) -> { - if (perms.containsKey(RolePermission.VIEW.getValue()) && perms.get(RolePermission.VIEW.getValue())) { + if (!perms.containsKey(RolePermission.VIEW.getValue())) { + return; + } + boolean viewPermission = perms.get(RolePermission.VIEW.getValue()); + if (viewPermission) { viewUsers.add(role); + } else { + negativeViewUsers.add(role); } }); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java index 2a045ffad9..7530e68d60 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java @@ -784,12 +784,9 @@ public void resolveUserRef(Case useCase) { @Override public Task resolveUserRef(Task task, Case useCase) { task.getUsers().clear(); - task.getNegativeViewUsers().clear(); task.getUserRefs().forEach((id, permission) -> { List userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(id).getValue()); - if (userIds != null && userIds.size() != 0 && permission.containsKey("view") && !permission.get("view")) { - task.getNegativeViewUsers().addAll(userIds); - } else if (userIds != null && userIds.size() != 0) { + if (userIds != null && !userIds.isEmpty()) { task.addUsers(new HashSet<>(userIds), permission); } }); diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java index 472df01d5a..d152707cfa 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/WorkflowService.java @@ -215,7 +215,6 @@ public long count(Map request, LoggedUser user, Locale locale) { @Override public Case resolveUserRef(Case useCase) { useCase.getUsers().clear(); - useCase.getNegativeViewUsers().clear(); useCase.getUserRefs().forEach((id, permission) -> { resolveUserRefPermissions(useCase, id, permission); }); @@ -226,12 +225,8 @@ public Case resolveUserRef(Case useCase) { private void resolveUserRefPermissions(Case useCase, String userListId, Map permission) { List userIds = getExistingUsers((UserListFieldValue) useCase.getDataSet().get(userListId).getValue()); - if (userIds != null && userIds.size() != 0) { - if (permission.containsKey("view") && !permission.get("view")) { - useCase.getNegativeViewUsers().addAll(userIds); - } else { - useCase.addUsers(new HashSet<>(userIds), permission); - } + if (userIds != null && !userIds.isEmpty()) { + useCase.addUsers(new HashSet<>(userIds), permission); } } diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy new file mode 100644 index 0000000000..5cbd7bd590 --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy @@ -0,0 +1,138 @@ +package com.netgrif.application.engine.workflow + +import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.auth.domain.IUser +import com.netgrif.application.engine.auth.domain.User +import com.netgrif.application.engine.auth.domain.UserState +import com.netgrif.application.engine.auth.service.interfaces.IUserService +import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService +import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest +import com.netgrif.application.engine.petrinet.domain.PetriNet +import com.netgrif.application.engine.petrinet.domain.VersionType +import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService +import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.domain.Task +import com.netgrif.application.engine.workflow.service.interfaces.IDataService +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService +import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.TaskSearchCaseRequest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.i18n.LocaleContextHolder +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension +import groovy.json.JsonBuilder + +import java.util.stream.Collectors + +@SpringBootTest +@ActiveProfiles(["test"]) +@ExtendWith(SpringExtension.class) +class TaskPermissionsTest { + + @Autowired + private IElasticTaskService elasticTaskService + + @Autowired + private IWorkflowService workflowService + + @Autowired + private IPetriNetService petriNetService + + @Autowired + private IUserService userService + + @Autowired + private IDataService dataService + + @Autowired + private ActionDelegate actionDelegate + + @Autowired + private TestHelper testHelper + + private static final String TEST_NET = "view_permission_combinations.xml" + private static final String TEST_NET_NO_DEFAULT = "view_permission_combinations_no_default.xml" + private static Case testCase + private static Case testCaseNoDefault + private static Map testUsers = [:] + + @BeforeEach() + void init() { + testHelper.truncateDbs() + actionDelegate.outcomes = [] + + PetriNet net = petriNetService.importPetriNet(new FileInputStream("src/test/resources/petriNets/" + TEST_NET), VersionType.MAJOR, userService.getLoggedOrSystem().transformToLoggedUser()).getNet() + PetriNet netNoDefault = petriNetService.importPetriNet(new FileInputStream("src/test/resources/petriNets/" + TEST_NET_NO_DEFAULT), VersionType.MAJOR, userService.getLoggedOrSystem().transformToLoggedUser()).getNet() + + testCase = workflowService.createCaseByIdentifier(net.identifier, "Test case", "", userService.getLoggedOrSystem().transformToLoggedUser()).getCase() + testCaseNoDefault = workflowService.createCaseByIdentifier(netNoDefault.identifier, "Test case with no default", "", userService.getLoggedOrSystem().transformToLoggedUser()).getCase() + + [ + new User("no_permissions@mail.com", "password", "No", "Permissions"), + new User("has_role@mail.com", "password", "Has", "Role"), + new User("in_userRef@mail.com", "password", "In", "UserRef"), + new User("both_permissions@mail.com", "password", "Both", "Permissions") + ].each { + it.setState(UserState.ACTIVE) + testUsers.put(it.getEmail(), userService.saveNew(it)) + } + + List withRoles = [testUsers.get("has_role@mail.com"), testUsers.get("both_permissions@mail.com")] + List inUserRef = [testUsers.get("in_userRef@mail.com").stringId, testUsers.get("both_permissions@mail.com").stringId] + + withRoles.forEach { + testUsers.put(it.getEmail(), userService.addRole(testUsers.get(it.getEmail()), net.roles.values().find { role -> role.importId == "process_role" }.stringId)) + testUsers.put(it.getEmail(), userService.addRole(testUsers.get(it.getEmail()), netNoDefault.roles.values().find { role -> role.importId == "process_role_no_default" }.stringId)) + } + + testCase = actionDelegate.setData("t_001", testCase, [ + "users": [ + "type": "userList", + "value": inUserRef + ] + ]).getCase() + + testCaseNoDefault = actionDelegate.setData("t_007", testCaseNoDefault, [ + "users": [ + "type": "userList", + "value": inUserRef + ] + ]).getCase() + def a = [] + } + + @Test + void testViewPermissions() { + def map = [:] + ElasticTaskSearchRequest request = new ElasticTaskSearchRequest() + request.useCase = [new TaskSearchCaseRequest(testCase.stringId, testCase.title)] + + ElasticTaskSearchRequest request2 = new ElasticTaskSearchRequest() + request2.useCase = [new TaskSearchCaseRequest(testCaseNoDefault.stringId, testCaseNoDefault.title)] + + testUsers.forEach( (key, value) -> { + Page tasks = elasticTaskService.search([request], + value.transformToLoggedUser(), + Pageable.unpaged(), LocaleContextHolder.getLocale(), false) + List list = new ArrayList<>(tasks.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() + + + Page tasks2 = elasticTaskService.search([request2], + value.transformToLoggedUser(), + Pageable.unpaged(), LocaleContextHolder.getLocale(), false) + List list2 = new ArrayList<>(tasks2.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() + + map.put(key, [ + "Default enabled": "$key -> number of found tasks with default role ENABLED: " + list.size() + "; tasks found: $list", + "Default DISabled": "$key -> number of found tasks with default role DISABLED: " + list2.size() + "; tasks found: $list2" + ]) + }) + println(new JsonBuilder(map).toPrettyString()) + } +} diff --git a/src/test/resources/petriNets/view_permission_combinations.xml b/src/test/resources/petriNets/view_permission_combinations.xml new file mode 100644 index 0000000000..8cf5d10d3d --- /dev/null +++ b/src/test/resources/petriNets/view_permission_combinations.xml @@ -0,0 +1,532 @@ + + + view_permission_combinations + 1.0.0 + VPC + View Permission Combinations + true + + + process_role + Process Role + + + + users + Users + + + + t_001 + 100 + 100 + + + process_role + + true + + + + default + + true + + + + users + + true + + + + + + t_002 + 300 + 100 + + + process_role + + true + + + + default + + true + + + + users + + false + + + + + + t_003 + 500 + 100 + + + process_role + + true + + + + default + + true + + + + + + t_004 + 700 + 100 + + + process_role + + true + + + + default + + false + + + + users + + true + + + + + + t_005 + 900 + 100 + + + process_role + + true + + + + default + + false + + + + users + + false + + + + + + t_006 + 1100 + 100 + + + process_role + + true + + + + default + + false + + + + + + t_007 + 1300 + 100 + + + process_role + + true + + + + users + + true + + + + + + t_008 + 1500 + 100 + + + process_role + + true + + + + users + + false + + + + + + t_009 + 1700 + 100 + + + process_role + + true + + + + + + t_010 + 100 + 300 + + + process_role + + false + + + + default + + true + + + + users + + true + + + + + + t_011 + 300 + 300 + + + process_role + + false + + + + default + + true + + + + users + + false + + + + + + t_012 + 500 + 300 + + + process_role + + false + + + + default + + true + + + + + + t_013 + 700 + 300 + + + process_role + + false + + + + default + + false + + + + users + + true + + + + + + t_014 + 900 + 300 + + + process_role + + false + + + + default + + false + + + + users + + false + + + + + + t_015 + 1100 + 300 + + + process_role + + false + + + + default + + false + + + + + + t_016 + 1300 + 300 + + + process_role + + false + + + + users + + true + + + + + + t_017 + 1500 + 300 + + + process_role + + false + + + + users + + false + + + + + + t_018 + 1700 + 300 + + + process_role + + false + + + + + + t_019 + 100 + 500 + + + default + + true + + + + users + + true + + + + + + t_020 + 300 + 500 + + + default + + true + + + + users + + false + + + + + + t_021 + 500 + 500 + + + default + + true + + + + + + t_022 + 700 + 500 + + + default + + false + + + + users + + true + + + + + + t_023 + 900 + 500 + + + default + + false + + + + users + + false + + + + + + t_024 + 1100 + 500 + + + default + + false + + + + + + t_025 + 1300 + 500 + + + users + + true + + + + + + t_026 + 1500 + 500 + + + users + + false + + + + + + t_027 + 1700 + 500 + + + \ No newline at end of file diff --git a/src/test/resources/petriNets/view_permission_combinations_no_default.xml b/src/test/resources/petriNets/view_permission_combinations_no_default.xml new file mode 100644 index 0000000000..0da52e1f84 --- /dev/null +++ b/src/test/resources/petriNets/view_permission_combinations_no_default.xml @@ -0,0 +1,154 @@ + + + view_permission_combinations_no_default + 1.0.0 + VPC + View Permission Combinations With Default Role Disabled + false + + + process_role + Process Role + + + + users + Users + + + + t_007 + 1300 + 100 + + + process_role + + true + + + + users + + true + + + + + + t_008 + 1500 + 100 + + + process_role + + true + + + + users + + false + + + + + + t_009 + 1700 + 100 + + + process_role + + true + + + + + + t_016 + 1300 + 300 + + + process_role + + false + + + + users + + true + + + + + + t_017 + 1500 + 300 + + + process_role + + false + + + + users + + false + + + + + + t_018 + 1700 + 300 + + + process_role + + false + + + + + + t_025 + 1300 + 500 + + + users + + true + + + + + + t_026 + 1500 + 500 + + + users + + false + + + + + + t_027 + 1700 + 500 + + + \ No newline at end of file From f0940111143debfffb08b2f64f7412746ef98fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Ma=C5=BE=C3=A1ri?= Date: Wed, 13 May 2026 09:47:59 +0200 Subject: [PATCH 2/5] [NAE-2424] - fix typo in role id --- .../application/engine/workflow/TaskPermissionsTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy index 5cbd7bd590..224ee54267 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy @@ -88,7 +88,7 @@ class TaskPermissionsTest { withRoles.forEach { testUsers.put(it.getEmail(), userService.addRole(testUsers.get(it.getEmail()), net.roles.values().find { role -> role.importId == "process_role" }.stringId)) - testUsers.put(it.getEmail(), userService.addRole(testUsers.get(it.getEmail()), netNoDefault.roles.values().find { role -> role.importId == "process_role_no_default" }.stringId)) + testUsers.put(it.getEmail(), userService.addRole(testUsers.get(it.getEmail()), netNoDefault.roles.values().find { role -> role.importId == "process_role" }.stringId)) } testCase = actionDelegate.setData("t_001", testCase, [ From 05593d07d8c2f549e89d60bc95eea45aaebebb6c Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 25 May 2026 12:06:35 +0200 Subject: [PATCH 3/5] [NAE-2424] UserRefs negative view permissions aren't resolved - refactored task view permission query resolution for both elastic search in ElasticViewPermissionService and for mongo in TaskSearchService - added mongo search test to TaskPermissionsTest --- .../elastic/service/ElasticTaskService.java | 12 -- .../service/ElasticViewPermissionService.java | 68 +++----- .../engine/importer/service/Importer.java | 20 +-- .../workflow/service/TaskSearchService.java | 25 +-- .../workflow/TaskPermissionsTest.groovy | 157 ++++++++++++++++-- ...permissions - correct default disabled.csv | 10 ++ .../resources/csv/permissions - correct.csv | 28 ++++ 7 files changed, 217 insertions(+), 103 deletions(-) create mode 100644 src/test/resources/csv/permissions - correct default disabled.csv create mode 100644 src/test/resources/csv/permissions - correct.csv diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java index cbb88b52fe..6eb471472c 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticTaskService.java @@ -178,7 +178,6 @@ protected BoolQueryBuilder buildSingleQuery(ElasticTaskSearchRequest request, Lo if (request == null) { throw new IllegalArgumentException("Request can not be null!"); } - addRolesQueryConstraint(request, user); BoolQueryBuilder query = boolQuery(); buildViewPermissionQuery(query, user); @@ -198,17 +197,6 @@ protected BoolQueryBuilder buildSingleQuery(ElasticTaskSearchRequest request, Lo return query; } - protected void addRolesQueryConstraint(ElasticTaskSearchRequest request, LoggedUser user) { - if (request.role != null && !request.role.isEmpty()) { - Set roles = new HashSet<>(request.role); - roles.addAll(user.getProcessRoles()); - request.role = new ArrayList<>(roles); - } else { - request.role = new ArrayList<>(user.getProcessRoles()); - } - } - - /** * Tasks of case with id "5cb07b6ff05be15f0b972c4d" * { diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java index b3c707b640..a6a6bf544a 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java @@ -8,85 +8,55 @@ public abstract class ElasticViewPermissionService { protected void buildViewPermissionQuery(BoolQueryBuilder query, LoggedUser user) { - BoolQueryBuilder viewPermsExists = boolQuery(); - BoolQueryBuilder viewPermNotExists = boolQuery(); - viewPermsExists.should(existsQuery("viewRoles")); - viewPermsExists.should(existsQuery("viewUserRefs")); - viewPermNotExists.mustNot(viewPermsExists); - /* Build positive view role query */ - BoolQueryBuilder positiveViewRole = buildPositiveViewRoleQuery(viewPermNotExists, user); +// (Rp!=0 & Rn = 0) + BoolQueryBuilder roleViewQuery = boolQuery() + .filter(buildPositiveViewRoleQuery(user)) + .mustNot(buildNegativeViewRoleQuery(user)); - /* Build negative view role query */ - BoolQueryBuilder negativeViewRole = buildNegativeViewRoleQuery(user); +// ((Rp!=0 & Rn = 0) or Up!=0) + BoolQueryBuilder roleOrPositiveUserQuery = boolQuery().should(roleViewQuery) + .should(buildPositiveViewUser(user)) + .minimumShouldMatch(1); - /* Positive view role set-minus negative view role */ - BoolQueryBuilder positiveRoleSetMinusNegativeRole = setMinus(positiveViewRole, negativeViewRole); - - /* Build positive view userList query */ - BoolQueryBuilder positiveViewUser = buildPositiveViewUser(viewPermNotExists, user); - - /* Role query union positive view userList */ - BoolQueryBuilder roleSetMinusPositiveUserList = union(positiveRoleSetMinusNegativeRole, positiveViewUser); - - /* Build negative view userList query */ - BoolQueryBuilder negativeViewUser = buildNegativeViewUser(user); - - /* Role-UserListPositive set-minus negative view userList */ - BoolQueryBuilder permissionQuery = setMinus(roleSetMinusPositiveUserList, negativeViewUser); - - query.filter(permissionQuery); +// (((Rp!=0 & Rn = 0) or Up!=0) & Un=0) == 1 + query.filter(roleOrPositiveUserQuery) + .mustNot(buildNegativeViewUser(user)); } - private BoolQueryBuilder buildPositiveViewRoleQuery(BoolQueryBuilder viewPermNotExists, LoggedUser user) { + private BoolQueryBuilder buildPositiveViewRoleQuery(LoggedUser user) { BoolQueryBuilder positiveViewRole = boolQuery(); BoolQueryBuilder positiveViewRoleQuery = boolQuery(); for (String roleId : user.getProcessRoles()) { positiveViewRoleQuery.should(termQuery("viewRoles", roleId)); } - positiveViewRole.should(viewPermNotExists); positiveViewRole.should(positiveViewRoleQuery); + positiveViewRole.minimumShouldMatch(1); return positiveViewRole; } private BoolQueryBuilder buildNegativeViewRoleQuery(LoggedUser user) { - BoolQueryBuilder negativeViewRole = boolQuery(); BoolQueryBuilder negativeViewRoleQuery = boolQuery(); for (String roleId : user.getProcessRoles()) { negativeViewRoleQuery.should(termQuery("negativeViewRoles", roleId)); } - negativeViewRole.mustNot(negativeViewRoleQuery); - return negativeViewRole; + negativeViewRoleQuery.minimumShouldMatch(1); + return negativeViewRoleQuery; } - private BoolQueryBuilder buildPositiveViewUser(BoolQueryBuilder viewPermNotExists, LoggedUser user) { + private BoolQueryBuilder buildPositiveViewUser(LoggedUser user) { BoolQueryBuilder positiveViewUser = boolQuery(); BoolQueryBuilder positiveViewUserQuery = boolQuery(); positiveViewUserQuery.must(termQuery("viewUsers", user.getId())); - positiveViewUser.should(viewPermNotExists); positiveViewUser.should(positiveViewUserQuery); + positiveViewUser.minimumShouldMatch(1); return positiveViewUser; } private BoolQueryBuilder buildNegativeViewUser(LoggedUser user) { - BoolQueryBuilder negativeViewUser = boolQuery(); BoolQueryBuilder negativeViewUserQuery = boolQuery(); negativeViewUserQuery.should(termQuery("negativeViewUsers", user.getId())); - negativeViewUser.mustNot(negativeViewUserQuery); - return negativeViewUser; - } - - private BoolQueryBuilder setMinus(BoolQueryBuilder positiveSet, BoolQueryBuilder negativeSet) { - BoolQueryBuilder positiveSetMinusNegativeSet = boolQuery(); - positiveSetMinusNegativeSet.must(positiveSet); - positiveSetMinusNegativeSet.must(negativeSet); - return positiveSetMinusNegativeSet; - } - - private BoolQueryBuilder union(BoolQueryBuilder setA, BoolQueryBuilder setB) { - BoolQueryBuilder unionSet = boolQuery(); - unionSet.should(setA); - unionSet.should(setB); - return unionSet; + negativeViewUserQuery.minimumShouldMatch(1); + return negativeViewUserQuery; } } diff --git a/src/main/java/com/netgrif/application/engine/importer/service/Importer.java b/src/main/java/com/netgrif/application/engine/importer/service/Importer.java index 596db2e301..1cef9be360 100644 --- a/src/main/java/com/netgrif/application/engine/importer/service/Importer.java +++ b/src/main/java/com/netgrif/application/engine/importer/service/Importer.java @@ -1156,11 +1156,8 @@ protected void addPredefinedRolesWithDefaultPermissions(com.netgrif.application. return; } } - // Don't add if positive roles or triggers or positive user refs - if ((importTransition.getRoleRef() != null && importTransition.getRoleRef().stream().anyMatch(this::hasPositivePermission)) - || (importTransition.getTrigger() != null && !importTransition.getTrigger().isEmpty()) - || (importTransition.getUsersRef() != null && importTransition.getUsersRef().stream().anyMatch(this::hasPositivePermission)) - || (importTransition.getUserRef() != null && importTransition.getUserRef().stream().anyMatch(this::hasPositivePermission))) { + + if (!importTransition.getRoleRef().isEmpty() || !importTransition.getUsersRef().isEmpty() || !importTransition.getUserRef().isEmpty()) { return; } @@ -1168,20 +1165,9 @@ protected void addPredefinedRolesWithDefaultPermissions(com.netgrif.application. addAnonymousRole(transition); } - protected boolean hasPositivePermission(PermissionRef permissionRef) { - return (permissionRef.getLogic().isPerform() != null && permissionRef.getLogic().isPerform()) - || (permissionRef.getLogic().isCancel() != null && permissionRef.getLogic().isCancel()) - || (permissionRef.getLogic().isView() != null && permissionRef.getLogic().isView()) - || (permissionRef.getLogic().isAssign() != null && permissionRef.getLogic().isAssign()) - || (permissionRef.getLogic().isAssigned() != null && permissionRef.getLogic().isAssigned()) - || (permissionRef.getLogic().isFinish() != null && permissionRef.getLogic().isFinish()) - || (permissionRef.getLogic().isDelegate() != null && permissionRef.getLogic().isDelegate()); - } - protected void addPredefinedRolesWithDefaultPermissions() { // only if no positive role associations and no positive user ref associations - if (net.getPermissions().values().stream().anyMatch(perms -> perms.containsValue(true)) - || net.getUserRefs().values().stream().anyMatch(perms -> perms.containsValue(true))) { + if (!net.getPermissions().isEmpty() || !net.getUserRefs().isEmpty()) { return; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskSearchService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskSearchService.java index aace6fb034..4c5fe9b4a3 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskSearchService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskSearchService.java @@ -41,16 +41,17 @@ public Predicate buildQuery(List requests, LoggedUser user, L BooleanBuilder builder = constructPredicateTree(singleQueries, isIntersection ? BooleanBuilder::and : BooleanBuilder::or); - BooleanBuilder constraints = new BooleanBuilder(buildRolesQueryConstraint(loggedOrImpersonated)); - constraints.or(buildUserRefQueryConstraint(loggedOrImpersonated)); - builder.and(constraints); - - BooleanBuilder permissionConstraints = new BooleanBuilder(buildViewRoleQueryConstraint(loggedOrImpersonated)); - permissionConstraints.andNot(buildNegativeViewRoleQueryConstraint(loggedOrImpersonated)); - permissionConstraints.or(buildViewUserQueryConstraint(loggedOrImpersonated)); - permissionConstraints.andNot(buildNegativeViewUsersQueryConstraint(loggedOrImpersonated)); - builder.and(permissionConstraints); - return builder; + // (Rp!=0 & Rn = 0) + BooleanBuilder constraints = new BooleanBuilder(buildViewRoleQueryConstraint(loggedOrImpersonated)) + .andNot(buildNegativeViewRoleQueryConstraint(loggedOrImpersonated)); + + // ((Rp!=0 & Rn = 0) or Up!=0) + constraints.or(buildViewUserQueryConstraint(loggedOrImpersonated)); + + // (((Rp!=0 & Rn = 0) or Up!=0) & Un=0) == 1 + constraints.andNot(buildNegativeViewUsersQueryConstraint(loggedOrImpersonated)); + + return builder.and(constraints); } protected Predicate buildRolesQueryConstraint(LoggedUser user) { @@ -69,7 +70,7 @@ protected Predicate buildViewRoleQueryConstraint(LoggedUser user) { } public Predicate viewRoleQuery(String role) { - return QTask.task.viewUserRefs.isEmpty().and(QTask.task.viewRoles.isEmpty()).or(QTask.task.viewRoles.contains(role)); + return QTask.task.viewRoles.contains(role); } protected Predicate buildViewUserQueryConstraint(LoggedUser user) { @@ -78,7 +79,7 @@ protected Predicate buildViewUserQueryConstraint(LoggedUser user) { } public Predicate viewUsersQuery(String userId) { - return QTask.task.negativeViewRoles.isEmpty().and(QTask.task.viewUserRefs.isEmpty()).and(QTask.task.viewRoles.isEmpty()).or(QTask.task.viewUsers.contains(userId)); + return QTask.task.viewUsers.contains(userId); } protected Predicate buildNegativeViewRoleQueryConstraint(LoggedUser user) { diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy index 224ee54267..c552888fe3 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy @@ -14,6 +14,7 @@ import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetServi import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.domain.Task import com.netgrif.application.engine.workflow.service.interfaces.IDataService +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.TaskSearchCaseRequest import org.junit.jupiter.api.BeforeEach @@ -26,7 +27,6 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension -import groovy.json.JsonBuilder import java.util.stream.Collectors @@ -38,6 +38,9 @@ class TaskPermissionsTest { @Autowired private IElasticTaskService elasticTaskService + @Autowired + private ITaskService taskService + @Autowired private IWorkflowService workflowService @@ -58,9 +61,12 @@ class TaskPermissionsTest { private static final String TEST_NET = "view_permission_combinations.xml" private static final String TEST_NET_NO_DEFAULT = "view_permission_combinations_no_default.xml" + private static final String CORRECT_PERMISSIONS_CSV_FILEPATH = "src/test/resources/csv/permissions - correct.csv" + private static final String CORRECT_PERMISSIONS_DEFAULT_DISABLED_CSV_FILEPATH = "src/test/resources/csv/permissions - correct default disabled.csv" private static Case testCase private static Case testCaseNoDefault private static Map testUsers = [:] + private static Map>> correctResults = new HashMap<>() @BeforeEach() void init() { @@ -93,46 +99,171 @@ class TaskPermissionsTest { testCase = actionDelegate.setData("t_001", testCase, [ "users": [ - "type": "userList", + "type" : "userList", "value": inUserRef ] ]).getCase() testCaseNoDefault = actionDelegate.setData("t_007", testCaseNoDefault, [ "users": [ - "type": "userList", + "type" : "userList", "value": inUserRef ] ]).getCase() - def a = [] + + correctResults = permissionsCsvToExpectedMap() + } + + static Map>> permissionsCsvToExpectedMap() { + Map>> expectedMap = initializeExpectedPermissionsMap() + + addPermissionsCsvToExpectedMap( + CORRECT_PERMISSIONS_CSV_FILEPATH, + "Default enabled", + expectedMap + ) + + addPermissionsCsvToExpectedMap( + CORRECT_PERMISSIONS_DEFAULT_DISABLED_CSV_FILEPATH, + "Default DISabled", + expectedMap + ) + + return expectedMap + } + + static void addPermissionsCsvToExpectedMap(String csvFilePath, + String defaultRoleKey, + Map>> expectedMap) { + File csvFile = new File(csvFilePath) + Map userColumns = userColumns() + + List lines = csvFile.readLines("UTF-8").findAll { it?.trim() } + List header = lines.first().split(",", -1)*.trim() + + int transitionIdIndex = header.indexOf("Transition ID") + + lines.tail().each { line -> + List columns = line.split(",", -1)*.trim() + String transitionId = columns[transitionIdIndex] + + userColumns.each { csvColumnName, userEmail -> + int permissionIndex = header.indexOf(csvColumnName) + + String permissionValue = columns[permissionIndex] + .replace(".", "") + .trim() + .toUpperCase() + + if (permissionValue == "TRUE") { + expectedMap[userEmail][defaultRoleKey] << transitionId + } + } + } + + expectedMap.values().each { Map> permissionsByDefaultRole -> + permissionsByDefaultRole[defaultRoleKey].sort() + } + } + + static Map>> initializeExpectedPermissionsMap() { + return userColumns().values().collectEntries { String email -> + [ + email, + [ + "Default enabled" : [], + "Default DISabled": [] + ] + ] + } + } + + static Map userColumns() { + return [ + "No permissions" : "no_permissions@mail.com", + "Has role" : "has_role@mail.com", + "Is in userList" : "in_userRef@mail.com", + "Has role and is in userList": "both_permissions@mail.com" + ] } @Test void testViewPermissions() { - def map = [:] + def mapElastic = [:] + def mapMongo = [:] +// todo test for both mongo and elastic ElasticTaskSearchRequest request = new ElasticTaskSearchRequest() request.useCase = [new TaskSearchCaseRequest(testCase.stringId, testCase.title)] ElasticTaskSearchRequest request2 = new ElasticTaskSearchRequest() request2.useCase = [new TaskSearchCaseRequest(testCaseNoDefault.stringId, testCaseNoDefault.title)] - testUsers.forEach( (key, value) -> { + testUsers.forEach((key, value) -> { +// Elastic task search Page tasks = elasticTaskService.search([request], value.transformToLoggedUser(), Pageable.unpaged(), LocaleContextHolder.getLocale(), false) - List list = new ArrayList<>(tasks.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() + List list = new ArrayList<>(tasks.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() Page tasks2 = elasticTaskService.search([request2], value.transformToLoggedUser(), Pageable.unpaged(), LocaleContextHolder.getLocale(), false) - List list2 = new ArrayList<>(tasks2.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() + List list2 = new ArrayList<>(tasks2.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() + +// Mongo task search + Page mongoTasksDefault = taskService.search([request], Pageable.unpaged(), value.transformToLoggedUser(), LocaleContextHolder.getLocale(), false) + List mongoListDefault = new ArrayList<>(mongoTasksDefault.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() + Page mongoTasksNoDefault = taskService.search([request2], Pageable.unpaged(), value.transformToLoggedUser(), LocaleContextHolder.getLocale(), false) + List mongoListNoDefault = new ArrayList<>(mongoTasksNoDefault.content).stream().map(task -> task.transitionId).collect(Collectors.toList()).sort() - map.put(key, [ - "Default enabled": "$key -> number of found tasks with default role ENABLED: " + list.size() + "; tasks found: $list", - "Default DISabled": "$key -> number of found tasks with default role DISABLED: " + list2.size() + "; tasks found: $list2" + mapElastic.put(key, [ + "Default enabled" : list, + "Default DISabled": list2 + ]) + + mapMongo.put(key, [ + "Default enabled" : mongoListDefault, + "Default DISabled": mongoListNoDefault ]) }) - println(new JsonBuilder(map).toPrettyString()) + compareTestResultsToExpected(mapElastic, "Elastic search") + compareTestResultsToExpected(mapMongo, "Mongo search") + } + + static void compareTestResultsToExpected(Map>> testResultMap, String searchType) { + println("\n========== ${searchType} - View permissions comparison ==========") + + testUsers.keySet().each { String userEmail -> + println("\nUser: ${userEmail}") + + ["Default enabled", "Default DISabled"].each { String defaultRoleKey -> + Set actualTransitionIds = new TreeSet<>( + testResultMap.get(userEmail)?.get(defaultRoleKey) ?: [] + ) + + Set expectedTransitionIds = new TreeSet<>( + correctResults.get(userEmail)?.get(defaultRoleKey) ?: [] + ) + + Set presentInBoth = new TreeSet<>(actualTransitionIds) + presentInBoth.retainAll(expectedTransitionIds) + + Set presentOnlyInMap = new TreeSet<>(actualTransitionIds) + presentOnlyInMap.removeAll(expectedTransitionIds) + + Set presentOnlyInCorrectResultsWithDefaultRoleMap = new TreeSet<>(expectedTransitionIds) + presentOnlyInCorrectResultsWithDefaultRoleMap.removeAll(actualTransitionIds) + + println("\n${searchType} - ${defaultRoleKey}:") + println("Present in both results (${presentInBoth.size()}): ${presentInBoth}") + println("Present only in test results (${presentOnlyInMap.size()}): ${presentOnlyInMap}") + println("Present only in correct results (${presentOnlyInCorrectResultsWithDefaultRoleMap.size()}): ${presentOnlyInCorrectResultsWithDefaultRoleMap}") + + assert presentInBoth.size() == actualTransitionIds.size() && presentInBoth.size() == expectedTransitionIds.size() + } + } + + println("\n=================================================") } -} +} \ No newline at end of file diff --git a/src/test/resources/csv/permissions - correct default disabled.csv b/src/test/resources/csv/permissions - correct default disabled.csv new file mode 100644 index 0000000000..bba1da1c6c --- /dev/null +++ b/src/test/resources/csv/permissions - correct default disabled.csv @@ -0,0 +1,10 @@ +Transition ID,Transition title,roleRef,userRef,No permissions,Has role,Is in userList,Has role and is in userList +t_007,role=true user=true,TRUE,TRUE,FALSE,TRUE,TRUE,TRUE +t_008,role=true user=false,TRUE,FALSE,FALSE,TRUE,FALSE,FALSE +t_009,role=true user=undefined,TRUE,undefined,FALSE,TRUE,FALSE,TRUE +t_016,role=false user=true,FALSE,TRUE,FALSE,FALSE,TRUE,TRUE +t_017,role=false user=false,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE +t_018,role=false user=undefined,FALSE,undefined,FALSE,FALSE,FALSE,FALSE +t_025,role=undefined user=true,undefined,TRUE,FALSE,FALSE,TRUE,TRUE +t_026,role=undefined user=false,undefined,FALSE,FALSE,FALSE,FALSE,FALSE +t_027,role=undefined user=undefined,undefined,undefined,FALSE,FALSE,FALSE,FALSE \ No newline at end of file diff --git a/src/test/resources/csv/permissions - correct.csv b/src/test/resources/csv/permissions - correct.csv new file mode 100644 index 0000000000..feb3ef6f84 --- /dev/null +++ b/src/test/resources/csv/permissions - correct.csv @@ -0,0 +1,28 @@ +Transition ID,Transition title,default role,roleRef,userRef,No permissions,Has role,Is in userList,Has role and is in userList +t_005,role=true default=false user=false,false,true,false,FALSE,FALSE,FALSE,FALSE +t_014,role=false default=false user=false,false,false,false,FALSE,FALSE,FALSE,FALSE +t_023,role=undefined default=false user=false,false,undefined,false,FALSE,FALSE,FALSE,FALSE +t_004,role=true default=false user=true,false,true,true,FALSE,FALSE,TRUE,TRUE +t_013,role=false default=false user=true,false,false,true,FALSE,FALSE,TRUE,TRUE +t_022,role=undefined default=false user=true,false,undefined,true,FALSE,FALSE,TRUE,TRUE +t_006,role=true default=false user=undefined,false,true,undefined,FALSE,FALSE,FALSE,FALSE +t_015,role=false default=false user=undefined,false,false,undefined,FALSE,FALSE,FALSE,FALSE +t_024,role=undefined default=false user=undefined,false,undefined,undefined,FALSE,FALSE,FALSE,FALSE +t_002,role=true default=true user=false,true,true,false,TRUE,TRUE,FALSE,FALSE +t_011,role=false default=true user=false,true,false,false,TRUE,FALSE,FALSE,FALSE +t_020,role=undefined default=true user=false,true,undefined,false,TRUE,TRUE,FALSE,FALSE +t_001,role=true default=true user=true,true,true,true,TRUE,TRUE,TRUE,TRUE +t_010,role=false default=true user=true,true,false,true,TRUE,FALSE,TRUE,TRUE +t_019,role=undefined default=true user=true,true,undefined,true,TRUE,TRUE,TRUE,TRUE +t_003,role=true default=true user=undefined,true,true,undefined,TRUE,TRUE,TRUE,TRUE +t_012,role=false default=true user=undefined,true,false,undefined,TRUE,FALSE,TRUE,FALSE +t_021,role=undefined default=true user=undefined,true,undefined,undefined,TRUE,TRUE,TRUE,TRUE +t_008,role=true default=undefined user=false,undefined,true,false,FALSE,TRUE,FALSE,FALSE +t_017,role=false default=undefined user=false,undefined,false,false,FALSE,FALSE,FALSE,FALSE +t_026,role=undefined default=undefined user=false,undefined,undefined,false,FALSE,FALSE,FALSE,FALSE +t_007,role=true default=undefined user=true,undefined,true,true,FALSE,TRUE,TRUE,TRUE +t_016,role=false default=undefined user=true,undefined,false,true,FALSE,FALSE,TRUE,TRUE +t_025,role=undefined default=undefined user=true,undefined,undefined,true,FALSE,FALSE,TRUE,TRUE +t_009,role=true default=undefined user=undefined,undefined,true,undefined,FALSE,TRUE,FALSE,TRUE +t_018,role=false default=undefined user=undefined,undefined,false,undefined,FALSE,FALSE,FALSE,FALSE +t_027,role=undefined default=undefined user=undefined,undefined,undefined,undefined,TRUE,TRUE,TRUE,TRUE \ No newline at end of file From cb6f7b83df0854353fcf599aeca7d867b6f18885 Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 25 May 2026 12:48:11 +0200 Subject: [PATCH 4/5] [NAE-2424] UserRefs negative view permissions aren't resolved - after merge fixes --- .../elastic/service/ElasticViewPermissionService.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java index 483081949c..578b63c563 100644 --- a/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java +++ b/src/main/java/com/netgrif/application/engine/elastic/service/ElasticViewPermissionService.java @@ -33,13 +33,12 @@ private BoolQueryBuilder buildPositiveViewRoleQuery(LoggedUser user) { if (!user.getProcessRoles().isEmpty()) { positiveViewRole.should(termsQuery("viewRoles", user.getProcessRoles())); } - positiveViewRole.should(positiveViewRole); positiveViewRole.minimumShouldMatch(1); return positiveViewRole; } /** - * Build a negative view role query by excluding negative roles. + * Build a negative view role query. */ private BoolQueryBuilder buildNegativeViewRoleQuery(LoggedUser user) { BoolQueryBuilder negativeViewRole = boolQuery(); @@ -51,7 +50,7 @@ private BoolQueryBuilder buildNegativeViewRoleQuery(LoggedUser user) { } /** - * Build a positive view user query using filter (as score is not needed). + * Build a positive view user query. */ private BoolQueryBuilder buildPositiveViewUser(LoggedUser user) { BoolQueryBuilder positiveViewUser = boolQuery(); @@ -61,7 +60,7 @@ private BoolQueryBuilder buildPositiveViewUser(LoggedUser user) { } /** - * Build a negative view user query to exclude the specified user. + * Build a negative view user query. */ private BoolQueryBuilder buildNegativeViewUser(LoggedUser user) { BoolQueryBuilder negativeViewUser = boolQuery(); From 87794e5d5ebd23f72e4619748360af2398d1742c Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 25 May 2026 13:19:15 +0200 Subject: [PATCH 5/5] [NAE-2424] UserRefs negative view permissions aren't resolved - null check added to Importer --- .../application/engine/importer/service/Importer.java | 4 +++- .../application/engine/workflow/TaskPermissionsTest.groovy | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/netgrif/application/engine/importer/service/Importer.java b/src/main/java/com/netgrif/application/engine/importer/service/Importer.java index 1cef9be360..a488d03643 100644 --- a/src/main/java/com/netgrif/application/engine/importer/service/Importer.java +++ b/src/main/java/com/netgrif/application/engine/importer/service/Importer.java @@ -1157,7 +1157,9 @@ protected void addPredefinedRolesWithDefaultPermissions(com.netgrif.application. } } - if (!importTransition.getRoleRef().isEmpty() || !importTransition.getUsersRef().isEmpty() || !importTransition.getUserRef().isEmpty()) { + if ((importTransition.getRoleRef() != null && !importTransition.getRoleRef().isEmpty()) || + (importTransition.getUsersRef() != null && !importTransition.getUsersRef().isEmpty()) || + (importTransition.getUserRef() != null && !importTransition.getUserRef().isEmpty())) { return; } diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy index c552888fe3..a2e4ee0b4d 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/TaskPermissionsTest.groovy @@ -72,6 +72,10 @@ class TaskPermissionsTest { void init() { testHelper.truncateDbs() actionDelegate.outcomes = [] + testUsers.clear() + correctResults.clear() + testCase = null + testCaseNoDefault = null PetriNet net = petriNetService.importPetriNet(new FileInputStream("src/test/resources/petriNets/" + TEST_NET), VersionType.MAJOR, userService.getLoggedOrSystem().transformToLoggedUser()).getNet() PetriNet netNoDefault = petriNetService.importPetriNet(new FileInputStream("src/test/resources/petriNets/" + TEST_NET_NO_DEFAULT), VersionType.MAJOR, userService.getLoggedOrSystem().transformToLoggedUser()).getNet() @@ -139,9 +143,11 @@ class TaskPermissionsTest { Map userColumns = userColumns() List lines = csvFile.readLines("UTF-8").findAll { it?.trim() } + assert !lines.isEmpty(): "CSV file is empty: ${csvFilePath}" List header = lines.first().split(",", -1)*.trim() int transitionIdIndex = header.indexOf("Transition ID") + assert transitionIdIndex >= 0: "Missing required column 'Transition ID' in ${csvFilePath}" lines.tail().each { line -> List columns = line.split(",", -1)*.trim() @@ -149,6 +155,7 @@ class TaskPermissionsTest { userColumns.each { csvColumnName, userEmail -> int permissionIndex = header.indexOf(csvColumnName) + assert permissionIndex >= 0: "Missing required column '${csvColumnName}' in ${csvFilePath}" String permissionValue = columns[permissionIndex] .replace(".", "")