Skip to content
Merged
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 @@ -27,6 +27,7 @@
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.security.ldap.LdapUserInfoProvider;
import io.takari.bpm.form.Form;

import javax.inject.Inject;
Expand All @@ -37,6 +38,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -47,10 +49,12 @@ public class FormAccessManager {
private static final Pattern GROUP_PATTERN = Pattern.compile("CN=(.*?),", Pattern.CASE_INSENSITIVE);

private final ProcessStateManager stateManager;
private final LdapUserInfoProvider ldapUserInfoProvider;

@Inject
public FormAccessManager(ProcessStateManager stateManager) {
public FormAccessManager(ProcessStateManager stateManager, LdapUserInfoProvider ldapUserInfoProvider) {
this.stateManager = stateManager;
this.ldapUserInfoProvider = ldapUserInfoProvider;
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -91,9 +95,7 @@ public void assertFormAccess(String formName, Map<String, Serializable> runAsPar

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> userLdapGroups = getLdapPrincipalGroups(p, ldapUserInfoProvider, LdapPrincipal::getCurrent);

boolean isGroupMatched = groups.stream()
.anyMatch(group -> matchesLdapGroup(group, userLdapGroups));
Expand All @@ -105,6 +107,23 @@ public void assertFormAccess(String formName, Map<String, Serializable> runAsPar
}
}

static Set<String> getLdapPrincipalGroups(UserPrincipal p,
LdapUserInfoProvider ldapUserInfoProvider,
Supplier<LdapPrincipal> currentPrincipalSupplier) {
if (p == null) {
return Set.of();
}

if (p.getRealm().equals("apikey")) {
// apikey realm doesn't look up groups by default, get them now
return ldapUserInfoProvider.getInfo(null, p.getUsername(), p.getDomain()).groups();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also UserManager#getCurrentUserInfo

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh that's tidier and should work.

}

return Optional.ofNullable(currentPrincipalSupplier.get())
.map(LdapPrincipal::getGroups)
.orElseGet(Set::of);
}

// TODO: move to the formManager
private Form getForm(ProcessKey processKey, String formName) {
String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,
Expand Down
Loading