Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 193 additions & 1 deletion it/server/src/test/java/com/walmartlabs/concord/it/server/LdapIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,32 @@
* =====
*/

import com.fasterxml.jackson.databind.ObjectMapper;
import com.walmartlabs.concord.client2.*;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

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;

Expand All @@ -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 {
Expand Down Expand Up @@ -92,7 +104,6 @@ public void testLdapUserGroups() throws Exception {
byte[] ab = getLog(pir.getInstanceId());
String groupDn = "cn=" + groupName + "," + GROUP_OU;
assertLog(".*" + groupDn + ".*", ab);

}

@Test
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand All @@ -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 {
Expand Down Expand Up @@ -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<InputStream> 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<String, Object> 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<InputStream> 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()));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
flows:
default:
- form: myForm
fields:
- inputName: { label: "name", type: "string" }
runAs:
ldap:
- group: "CN=${ldapGroupName},.*"
- log: "Submitted name: ${myForm.inputName}"
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")
Expand Down Expand Up @@ -89,18 +92,18 @@ public void assertFormAccess(String formName, Map<String, Serializable> runAsPar
"the necessary permissions to access the form.");
}

Set<String> groups = com.walmartlabs.concord.forms.FormUtils.getRunAsLdapGroups(formName, runAsParams);
if (!groups.isEmpty()) {
Set<String> userLdapGroups = Optional.ofNullable(LdapPrincipal.getCurrent())
.map(LdapPrincipal::getGroups)
.orElse(null);
Set<String> formRunAsGroups = com.walmartlabs.concord.forms.FormUtils.getRunAsLdapGroups(formName, runAsParams);
if (!formRunAsGroups.isEmpty()) {
Set<String> 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 + "'");
}
}
}
Expand Down