diff --git a/it/server/src/test/java/com/walmartlabs/concord/it/server/LdapIT.java b/it/server/src/test/java/com/walmartlabs/concord/it/server/LdapIT.java index 693bf0cf12..ad7091ef91 100644 --- a/it/server/src/test/java/com/walmartlabs/concord/it/server/LdapIT.java +++ b/it/server/src/test/java/com/walmartlabs/concord/it/server/LdapIT.java @@ -19,6 +19,7 @@ * ===== */ +import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.client2.*; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -26,14 +27,24 @@ import javax.naming.Context; import javax.naming.NameAlreadyBoundException; import javax.naming.directory.*; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Base64; +import java.util.Map; import java.util.Properties; +import java.util.UUID; import static com.walmartlabs.concord.it.common.ITUtils.archive; import static com.walmartlabs.concord.it.common.ServerClient.assertLog; import static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion; +import static com.walmartlabs.concord.it.common.ServerClient.waitForStatus; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -44,6 +55,7 @@ public class LdapIT extends AbstractServerIT { private static final String GROUP_OU = "ou=groups,dc=example,dc=org"; private static final String USER_OU = "ou=users,dc=example,dc=org"; private static DirContext ldapCtx; + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().build(); @BeforeAll public static void createLdapStructure() throws Exception { @@ -92,7 +104,6 @@ public void testLdapUserGroups() throws Exception { byte[] ab = getLog(pir.getInstanceId()); String groupDn = "cn=" + groupName + "," + GROUP_OU; assertLog(".*" + groupDn + ".*", ab); - } @Test @@ -136,6 +147,140 @@ void testDisableLdapUser() throws Exception { assertTrue(ue.getPermanentlyDisabled()); } + @Test + void testSubmitFormRunAsGroupWithApiKey() throws Exception { + // create users in ldap + String noGroupUser = "noGroupUser" + randomString(); + createLdapUser(noGroupUser); + + String username = "runAsUser" + randomString(); + createLdapUser(username); + + // create group + String groupName = "RunAsGroup" + randomString(); + createLdapGroupWithUser(groupName, username); + + UsersApi usersApi = new UsersApi(getApiClient()); + usersApi.createOrUpdateUser(new CreateUserRequest() + .username(username) + .type(CreateUserRequest.TypeEnum.LDAP)); + usersApi.createOrUpdateUser(new CreateUserRequest() + .username(noGroupUser) + .type(CreateUserRequest.TypeEnum.LDAP)); + + String noGroupApiKey = createApiKey(noGroupUser); + String validUserApiKey = createApiKey(username); + + setApiKey(validUserApiKey); + + // --- execute form + + byte[] payload = archive(LdapIT.class.getResource("ldapFormRunAs").toURI()); + StartProcessResponse spr = start(Map.of( + "archive", payload, + "arguments.ldapGroupName", groupName + )); + assertNotNull(spr.getInstanceId()); + + + // --- + + ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED); + + // --- try to get with user not in group (expect no permission) + + ApiException noGroupEx = assertThrows(ApiException.class, () -> + new ProcessFormsApi(getApiClientForKey(noGroupApiKey)).getProcessForm(pir.getInstanceId(), "myForm")); + + assertEquals(403, noGroupEx.getCode()); + assertTrue(noGroupEx.getMessage().contains("doesn't have the necessary permissions to resume process. Expected LDAP group(s) '[CN=RunAsGroup")); + + // --- get form with user in expected ldap group + + ProcessFormsApi formsApi = new ProcessFormsApi(getApiClientForKey(validUserApiKey)); + + FormInstanceEntry form = formsApi.getProcessForm(pir.getInstanceId(), "myForm"); + + assertEquals("myForm", form.getName()); + assertEquals(1, form.getFields().size()); + assertEquals("inputName", form.getFields().get(0).getName()); + assertEquals("string", form.getFields().get(0).getType()); + + // --- submit form with user in expected ldap group + + formsApi.submitForm(pir.getInstanceId(), "myForm", Map.of("inputName", "testuser")); + + waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED); + + byte[] ab = getLog(pir.getInstanceId()); + assertLog(".*Submitted name: testuser.*", ab); + } + + @Test + void testSubmitFormRunAsGroupWithPassword() throws Exception { + // create users in ldap + String noGroupUser = "noGroupUser" + randomString(); + createLdapUser(noGroupUser); + + String username = "runAsUser" + randomString(); + createLdapUser(username); + + // create group + String groupName = "RunAsGroup" + randomString(); + createLdapGroupWithUser(groupName, username); + + UsersApi usersApi = new UsersApi(getApiClient()); + usersApi.createOrUpdateUser(new CreateUserRequest() + .username(username) + .type(CreateUserRequest.TypeEnum.LDAP)); + usersApi.createOrUpdateUser(new CreateUserRequest() + .username(noGroupUser) + .type(CreateUserRequest.TypeEnum.LDAP)); + + String validUserApiKey = createApiKey(username); + + setApiKey(validUserApiKey); + + // --- execute form + + byte[] payload = archive(LdapIT.class.getResource("ldapFormRunAs").toURI()); + StartProcessResponse spr = start(Map.of( + "archive", payload, + "arguments.ldapGroupName", groupName + )); + assertNotNull(spr.getInstanceId()); + + // --- + + ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED); + + // --- try to get with user not in group (expect no permission) + + ApiException noGroupEx = assertThrows(ApiException.class, () -> + getFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), "myForm", noGroupUser, noGroupUser)); + + assertEquals(403, noGroupEx.getCode()); + assertTrue(noGroupEx.getResponseBody().contains("doesn't have the necessary permissions to resume process. Expected LDAP group(s) '[CN=RunAsGroup")); + + // --- get form with user in expected ldap group + + FormInstanceEntry form = getFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), "myForm", username, username); + + assertEquals("myForm", form.getName()); + assertEquals(1, form.getFields().size()); + assertEquals("inputName", form.getFields().get(0).getName()); + assertEquals("string", form.getFields().get(0).getType()); + + // --- submit form with user in expected ldap group + + submitFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), "myForm", Map.of("inputName", "testuser"), username, username); + + waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED); + + byte[] ab = getLog(pir.getInstanceId()); + assertLog(".*Submitted name: testuser.*", ab); + } + public static DirContext createContext() throws Exception { String url = System.getenv("IT_LDAP_URL"); String connectionType = "simple"; @@ -176,6 +321,7 @@ private static void createLdapUser(String username) throws Exception { Attribute uid = new BasicAttribute("uid", username); Attribute cn = new BasicAttribute(COMMON_NAME, username); Attribute sn = new BasicAttribute("sn", username); + Attribute userPassword = new BasicAttribute("userPassword", username); Attribute objectClass = new BasicAttribute(OBJECT_CLASS); objectClass.add("top"); @@ -186,6 +332,7 @@ private static void createLdapUser(String username) throws Exception { attributes.put(uid); attributes.put(cn); attributes.put(sn); + attributes.put(userPassword); attributes.put(objectClass); try { @@ -216,4 +363,49 @@ private static void createLdapGroupWithUser(String groupName, String username) t // already exists, ignore } } + + private String createApiKey(String username) throws Exception { + ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient()); + CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest() + .username(username) + .userType(CreateApiKeyRequest.UserTypeEnum.LDAP)); + + return cakr.getKey(); + } + + private FormInstanceEntry getFormHttpClient(String baseUrl, UUID instanceId, String formName, String username, String password) throws Exception { + HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + "/api/v1/process/" + instanceId + "/form/" + formName)) + .GET() + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes())) + .build(); + + HttpResponse resp = HTTP_CLIENT.send(req, HttpResponse.BodyHandlers.ofInputStream()); + + try (InputStream is = resp.body()) { + if (resp.statusCode() != 200) { + throw new ApiException(resp.statusCode(), resp.headers(), new String(is.readAllBytes())); + } + + return new ObjectMapper().readValue(resp.body(), FormInstanceEntry.class); + } + } + + private void submitFormHttpClient(String baseUrl, UUID instanceId, String formName, Map data, String username, String password) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + String requestBody = mapper.writeValueAsString(data); + + HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + "/api/v1/process/" + instanceId + "/form/" + formName)) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .header("Content-Type", "application/json") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes())) + .build(); + + HttpResponse resp = HTTP_CLIENT.send(req, HttpResponse.BodyHandlers.ofInputStream()); + + try (InputStream is = resp.body()) { + if (resp.statusCode() != 200) { + throw new ApiException(resp.statusCode(), resp.headers(), new String(resp.body().readAllBytes())); + } + } + } } diff --git a/it/server/src/test/resources/com/walmartlabs/concord/it/server/ldapFormRunAs/concord.yml b/it/server/src/test/resources/com/walmartlabs/concord/it/server/ldapFormRunAs/concord.yml new file mode 100644 index 0000000000..6028468788 --- /dev/null +++ b/it/server/src/test/resources/com/walmartlabs/concord/it/server/ldapFormRunAs/concord.yml @@ -0,0 +1,9 @@ +flows: + default: + - form: myForm + fields: + - inputName: { label: "name", type: "string" } + runAs: + ldap: + - group: "CN=${ldapGroupName},.*" + - log: "Submitted name: ${myForm.inputName}" diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormAccessManager.java b/server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormAccessManager.java index 1c4ab0d706..40f8bd8d11 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormAccessManager.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormAccessManager.java @@ -26,7 +26,8 @@ import com.walmartlabs.concord.server.security.Roles; import com.walmartlabs.concord.server.security.UnauthorizedException; import com.walmartlabs.concord.server.security.UserPrincipal; -import com.walmartlabs.concord.server.security.ldap.LdapPrincipal; +import com.walmartlabs.concord.server.user.UserInfoProvider; +import com.walmartlabs.concord.server.user.UserManager; import io.takari.bpm.form.Form; import javax.inject.Inject; @@ -47,10 +48,12 @@ public class FormAccessManager { private static final Pattern GROUP_PATTERN = Pattern.compile("CN=(.*?),", Pattern.CASE_INSENSITIVE); private final ProcessStateManager stateManager; + private final UserManager userManager; @Inject - public FormAccessManager(ProcessStateManager stateManager) { + public FormAccessManager(ProcessStateManager stateManager, UserManager userManager) { this.stateManager = stateManager; + this.userManager = userManager; } @SuppressWarnings("unchecked") @@ -89,18 +92,18 @@ public void assertFormAccess(String formName, Map runAsPar "the necessary permissions to access the form."); } - Set groups = com.walmartlabs.concord.forms.FormUtils.getRunAsLdapGroups(formName, runAsParams); - if (!groups.isEmpty()) { - Set userLdapGroups = Optional.ofNullable(LdapPrincipal.getCurrent()) - .map(LdapPrincipal::getGroups) - .orElse(null); + Set formRunAsGroups = com.walmartlabs.concord.forms.FormUtils.getRunAsLdapGroups(formName, runAsParams); + if (!formRunAsGroups.isEmpty()) { + Set userLdapGroups = Optional.ofNullable(userManager.getCurrentUserInfo()) + .map(UserInfoProvider.UserInfo::groups) + .orElseGet(Set::of); - boolean isGroupMatched = groups.stream() + boolean isGroupMatched = formRunAsGroups.stream() .anyMatch(group -> matchesLdapGroup(group, userLdapGroups)); if (!isGroupMatched) { throw new UnauthorizedException("The current user (" + p.getUsername() + ") doesn't have " + - "the necessary permissions to resume process. Expected LDAP group(s) '" + groups + "'"); + "the necessary permissions to resume process. Expected LDAP group(s) '" + formRunAsGroups + "'"); } } }