Skip to content

Commit 035cd79

Browse files
authored
concord-server: retrieve user ldap groups for form access assertion (#1233)
1 parent 211aa70 commit 035cd79

File tree

3 files changed

+214
-10
lines changed
  • it/server/src/test
    • java/com/walmartlabs/concord/it/server
    • resources/com/walmartlabs/concord/it/server/ldapFormRunAs
  • server/impl/src/main/java/com/walmartlabs/concord/server/process/form

3 files changed

+214
-10
lines changed

it/server/src/test/java/com/walmartlabs/concord/it/server/LdapIT.java

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,32 @@
1919
* =====
2020
*/
2121

22+
import com.fasterxml.jackson.databind.ObjectMapper;
2223
import com.walmartlabs.concord.client2.*;
2324
import org.junit.jupiter.api.BeforeAll;
2425
import org.junit.jupiter.api.Test;
2526

2627
import javax.naming.Context;
2728
import javax.naming.NameAlreadyBoundException;
2829
import javax.naming.directory.*;
30+
import java.io.InputStream;
31+
import java.net.URI;
32+
import java.net.http.HttpClient;
33+
import java.net.http.HttpRequest;
34+
import java.net.http.HttpResponse;
35+
import java.util.Base64;
36+
import java.util.Map;
2937
import java.util.Properties;
38+
import java.util.UUID;
3039

3140
import static com.walmartlabs.concord.it.common.ITUtils.archive;
3241
import static com.walmartlabs.concord.it.common.ServerClient.assertLog;
3342
import static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;
43+
import static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;
3444
import static org.junit.jupiter.api.Assertions.assertEquals;
3545
import static org.junit.jupiter.api.Assertions.assertFalse;
3646
import static org.junit.jupiter.api.Assertions.assertNotNull;
47+
import static org.junit.jupiter.api.Assertions.assertThrows;
3748
import static org.junit.jupiter.api.Assertions.assertTrue;
3849
import static org.junit.jupiter.api.Assumptions.assumeTrue;
3950

@@ -44,6 +55,7 @@ public class LdapIT extends AbstractServerIT {
4455
private static final String GROUP_OU = "ou=groups,dc=example,dc=org";
4556
private static final String USER_OU = "ou=users,dc=example,dc=org";
4657
private static DirContext ldapCtx;
58+
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().build();
4759

4860
@BeforeAll
4961
public static void createLdapStructure() throws Exception {
@@ -92,7 +104,6 @@ public void testLdapUserGroups() throws Exception {
92104
byte[] ab = getLog(pir.getInstanceId());
93105
String groupDn = "cn=" + groupName + "," + GROUP_OU;
94106
assertLog(".*" + groupDn + ".*", ab);
95-
96107
}
97108

98109
@Test
@@ -136,6 +147,140 @@ void testDisableLdapUser() throws Exception {
136147
assertTrue(ue.getPermanentlyDisabled());
137148
}
138149

150+
@Test
151+
void testSubmitFormRunAsGroupWithApiKey() throws Exception {
152+
// create users in ldap
153+
String noGroupUser = "noGroupUser" + randomString();
154+
createLdapUser(noGroupUser);
155+
156+
String username = "runAsUser" + randomString();
157+
createLdapUser(username);
158+
159+
// create group
160+
String groupName = "RunAsGroup" + randomString();
161+
createLdapGroupWithUser(groupName, username);
162+
163+
UsersApi usersApi = new UsersApi(getApiClient());
164+
usersApi.createOrUpdateUser(new CreateUserRequest()
165+
.username(username)
166+
.type(CreateUserRequest.TypeEnum.LDAP));
167+
usersApi.createOrUpdateUser(new CreateUserRequest()
168+
.username(noGroupUser)
169+
.type(CreateUserRequest.TypeEnum.LDAP));
170+
171+
String noGroupApiKey = createApiKey(noGroupUser);
172+
String validUserApiKey = createApiKey(username);
173+
174+
setApiKey(validUserApiKey);
175+
176+
// --- execute form
177+
178+
byte[] payload = archive(LdapIT.class.getResource("ldapFormRunAs").toURI());
179+
StartProcessResponse spr = start(Map.of(
180+
"archive", payload,
181+
"arguments.ldapGroupName", groupName
182+
));
183+
assertNotNull(spr.getInstanceId());
184+
185+
186+
// ---
187+
188+
ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);
189+
190+
// --- try to get with user not in group (expect no permission)
191+
192+
ApiException noGroupEx = assertThrows(ApiException.class, () ->
193+
new ProcessFormsApi(getApiClientForKey(noGroupApiKey)).getProcessForm(pir.getInstanceId(), "myForm"));
194+
195+
assertEquals(403, noGroupEx.getCode());
196+
assertTrue(noGroupEx.getMessage().contains("doesn't have the necessary permissions to resume process. Expected LDAP group(s) '[CN=RunAsGroup"));
197+
198+
// --- get form with user in expected ldap group
199+
200+
ProcessFormsApi formsApi = new ProcessFormsApi(getApiClientForKey(validUserApiKey));
201+
202+
FormInstanceEntry form = formsApi.getProcessForm(pir.getInstanceId(), "myForm");
203+
204+
assertEquals("myForm", form.getName());
205+
assertEquals(1, form.getFields().size());
206+
assertEquals("inputName", form.getFields().get(0).getName());
207+
assertEquals("string", form.getFields().get(0).getType());
208+
209+
// --- submit form with user in expected ldap group
210+
211+
formsApi.submitForm(pir.getInstanceId(), "myForm", Map.of("inputName", "testuser"));
212+
213+
waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);
214+
215+
byte[] ab = getLog(pir.getInstanceId());
216+
assertLog(".*Submitted name: testuser.*", ab);
217+
}
218+
219+
@Test
220+
void testSubmitFormRunAsGroupWithPassword() throws Exception {
221+
// create users in ldap
222+
String noGroupUser = "noGroupUser" + randomString();
223+
createLdapUser(noGroupUser);
224+
225+
String username = "runAsUser" + randomString();
226+
createLdapUser(username);
227+
228+
// create group
229+
String groupName = "RunAsGroup" + randomString();
230+
createLdapGroupWithUser(groupName, username);
231+
232+
UsersApi usersApi = new UsersApi(getApiClient());
233+
usersApi.createOrUpdateUser(new CreateUserRequest()
234+
.username(username)
235+
.type(CreateUserRequest.TypeEnum.LDAP));
236+
usersApi.createOrUpdateUser(new CreateUserRequest()
237+
.username(noGroupUser)
238+
.type(CreateUserRequest.TypeEnum.LDAP));
239+
240+
String validUserApiKey = createApiKey(username);
241+
242+
setApiKey(validUserApiKey);
243+
244+
// --- execute form
245+
246+
byte[] payload = archive(LdapIT.class.getResource("ldapFormRunAs").toURI());
247+
StartProcessResponse spr = start(Map.of(
248+
"archive", payload,
249+
"arguments.ldapGroupName", groupName
250+
));
251+
assertNotNull(spr.getInstanceId());
252+
253+
// ---
254+
255+
ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);
256+
257+
// --- try to get with user not in group (expect no permission)
258+
259+
ApiException noGroupEx = assertThrows(ApiException.class, () ->
260+
getFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), "myForm", noGroupUser, noGroupUser));
261+
262+
assertEquals(403, noGroupEx.getCode());
263+
assertTrue(noGroupEx.getResponseBody().contains("doesn't have the necessary permissions to resume process. Expected LDAP group(s) '[CN=RunAsGroup"));
264+
265+
// --- get form with user in expected ldap group
266+
267+
FormInstanceEntry form = getFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), "myForm", username, username);
268+
269+
assertEquals("myForm", form.getName());
270+
assertEquals(1, form.getFields().size());
271+
assertEquals("inputName", form.getFields().get(0).getName());
272+
assertEquals("string", form.getFields().get(0).getType());
273+
274+
// --- submit form with user in expected ldap group
275+
276+
submitFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), "myForm", Map.of("inputName", "testuser"), username, username);
277+
278+
waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);
279+
280+
byte[] ab = getLog(pir.getInstanceId());
281+
assertLog(".*Submitted name: testuser.*", ab);
282+
}
283+
139284
public static DirContext createContext() throws Exception {
140285
String url = System.getenv("IT_LDAP_URL");
141286
String connectionType = "simple";
@@ -176,6 +321,7 @@ private static void createLdapUser(String username) throws Exception {
176321
Attribute uid = new BasicAttribute("uid", username);
177322
Attribute cn = new BasicAttribute(COMMON_NAME, username);
178323
Attribute sn = new BasicAttribute("sn", username);
324+
Attribute userPassword = new BasicAttribute("userPassword", username);
179325

180326
Attribute objectClass = new BasicAttribute(OBJECT_CLASS);
181327
objectClass.add("top");
@@ -186,6 +332,7 @@ private static void createLdapUser(String username) throws Exception {
186332
attributes.put(uid);
187333
attributes.put(cn);
188334
attributes.put(sn);
335+
attributes.put(userPassword);
189336
attributes.put(objectClass);
190337

191338
try {
@@ -216,4 +363,49 @@ private static void createLdapGroupWithUser(String groupName, String username) t
216363
// already exists, ignore
217364
}
218365
}
366+
367+
private String createApiKey(String username) throws Exception {
368+
ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());
369+
CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()
370+
.username(username)
371+
.userType(CreateApiKeyRequest.UserTypeEnum.LDAP));
372+
373+
return cakr.getKey();
374+
}
375+
376+
private FormInstanceEntry getFormHttpClient(String baseUrl, UUID instanceId, String formName, String username, String password) throws Exception {
377+
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + "/api/v1/process/" + instanceId + "/form/" + formName))
378+
.GET()
379+
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()))
380+
.build();
381+
382+
HttpResponse<InputStream> resp = HTTP_CLIENT.send(req, HttpResponse.BodyHandlers.ofInputStream());
383+
384+
try (InputStream is = resp.body()) {
385+
if (resp.statusCode() != 200) {
386+
throw new ApiException(resp.statusCode(), resp.headers(), new String(is.readAllBytes()));
387+
}
388+
389+
return new ObjectMapper().readValue(resp.body(), FormInstanceEntry.class);
390+
}
391+
}
392+
393+
private void submitFormHttpClient(String baseUrl, UUID instanceId, String formName, Map<String, Object> data, String username, String password) throws Exception {
394+
ObjectMapper mapper = new ObjectMapper();
395+
String requestBody = mapper.writeValueAsString(data);
396+
397+
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + "/api/v1/process/" + instanceId + "/form/" + formName))
398+
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
399+
.header("Content-Type", "application/json")
400+
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()))
401+
.build();
402+
403+
HttpResponse<InputStream> resp = HTTP_CLIENT.send(req, HttpResponse.BodyHandlers.ofInputStream());
404+
405+
try (InputStream is = resp.body()) {
406+
if (resp.statusCode() != 200) {
407+
throw new ApiException(resp.statusCode(), resp.headers(), new String(resp.body().readAllBytes()));
408+
}
409+
}
410+
}
219411
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
flows:
2+
default:
3+
- form: myForm
4+
fields:
5+
- inputName: { label: "name", type: "string" }
6+
runAs:
7+
ldap:
8+
- group: "CN=${ldapGroupName},.*"
9+
- log: "Submitted name: ${myForm.inputName}"

server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormAccessManager.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
import com.walmartlabs.concord.server.security.Roles;
2727
import com.walmartlabs.concord.server.security.UnauthorizedException;
2828
import com.walmartlabs.concord.server.security.UserPrincipal;
29-
import com.walmartlabs.concord.server.security.ldap.LdapPrincipal;
29+
import com.walmartlabs.concord.server.user.UserInfoProvider;
30+
import com.walmartlabs.concord.server.user.UserManager;
3031
import io.takari.bpm.form.Form;
3132

3233
import javax.inject.Inject;
@@ -47,10 +48,12 @@ public class FormAccessManager {
4748
private static final Pattern GROUP_PATTERN = Pattern.compile("CN=(.*?),", Pattern.CASE_INSENSITIVE);
4849

4950
private final ProcessStateManager stateManager;
51+
private final UserManager userManager;
5052

5153
@Inject
52-
public FormAccessManager(ProcessStateManager stateManager) {
54+
public FormAccessManager(ProcessStateManager stateManager, UserManager userManager) {
5355
this.stateManager = stateManager;
56+
this.userManager = userManager;
5457
}
5558

5659
@SuppressWarnings("unchecked")
@@ -89,18 +92,18 @@ public void assertFormAccess(String formName, Map<String, Serializable> runAsPar
8992
"the necessary permissions to access the form.");
9093
}
9194

92-
Set<String> groups = com.walmartlabs.concord.forms.FormUtils.getRunAsLdapGroups(formName, runAsParams);
93-
if (!groups.isEmpty()) {
94-
Set<String> userLdapGroups = Optional.ofNullable(LdapPrincipal.getCurrent())
95-
.map(LdapPrincipal::getGroups)
96-
.orElse(null);
95+
Set<String> formRunAsGroups = com.walmartlabs.concord.forms.FormUtils.getRunAsLdapGroups(formName, runAsParams);
96+
if (!formRunAsGroups.isEmpty()) {
97+
Set<String> userLdapGroups = Optional.ofNullable(userManager.getCurrentUserInfo())
98+
.map(UserInfoProvider.UserInfo::groups)
99+
.orElseGet(Set::of);
97100

98-
boolean isGroupMatched = groups.stream()
101+
boolean isGroupMatched = formRunAsGroups.stream()
99102
.anyMatch(group -> matchesLdapGroup(group, userLdapGroups));
100103

101104
if (!isGroupMatched) {
102105
throw new UnauthorizedException("The current user (" + p.getUsername() + ") doesn't have " +
103-
"the necessary permissions to resume process. Expected LDAP group(s) '" + groups + "'");
106+
"the necessary permissions to resume process. Expected LDAP group(s) '" + formRunAsGroups + "'");
104107
}
105108
}
106109
}

0 commit comments

Comments
 (0)