Skip to content

Commit db925a4

Browse files
committed
Support EL interpolation in group's external id
Reuse logic from autoCreateTestUsers. This closes #819
1 parent 3e33c2e commit db925a4

File tree

8 files changed

+90
-40
lines changed

8 files changed

+90
-40
lines changed

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImpl.java

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
import biz.netcentric.cq.tools.actool.authorizableinstaller.AuthorizableCreatorException;
3939
import biz.netcentric.cq.tools.actool.configmodel.AuthorizableConfigBean;
40+
import biz.netcentric.cq.tools.actool.configreader.YamlMacroElEvaluator;
4041
import biz.netcentric.cq.tools.actool.helper.Constants;
4142
import biz.netcentric.cq.tools.actool.history.InstallationLogger;
4243

@@ -59,11 +60,8 @@ public Authorizable createGroupWithExternalId(
5960
throws AuthorizableExistsException, RepositoryException,
6061
AuthorizableCreatorException {
6162

62-
if (StringUtils.isBlank(authorizableConfigBean.getExternalId())) {
63-
throw new IllegalStateException("externalId must not be empty for " + authorizableConfigBean);
64-
}
65-
66-
ExternalGroup externalGroup = new PrecreatedExternalGroup(authorizableConfigBean);
63+
String externalId = getExternalId(authorizableConfigBean);
64+
ExternalGroup externalGroup = new PrecreatedExternalGroup(authorizableConfigBean.getAuthorizableId(), externalId, authorizableConfigBean.getPath());
6765

6866
ExternalGroupPrecreatorSyncContext externalGroupPrecreatorSyncContext = new ExternalGroupPrecreatorSyncContext(userManager,
6967
session.getValueFactory());
@@ -72,6 +70,15 @@ public Authorizable createGroupWithExternalId(
7270
return group;
7371
}
7472

73+
static String getExternalId(final AuthorizableConfigBean authorizableConfigBean) {
74+
YamlMacroElEvaluator elEvaluator = new YamlMacroElEvaluator();
75+
String externalId = elEvaluator.evaluateElWithPercentSyntax(authorizableConfigBean.getExternalId(), String.class, authorizableConfigBean.getVariablesForInterpolation());
76+
if (StringUtils.isBlank(externalId)) {
77+
throw new IllegalStateException("externalId must not be empty for " + authorizableConfigBean);
78+
}
79+
return externalId;
80+
}
81+
7582
// simple workaround to make protected method available here
7683
private final class ExternalGroupPrecreatorSyncContext extends DefaultSyncContext {
7784

@@ -87,33 +94,36 @@ private Group createExternalGroup(ExternalGroup eg) throws RepositoryException {
8794

8895
// mapping AuthorizableConfigBean -> ExternalGroup
8996
private final class PrecreatedExternalGroup implements ExternalGroup {
90-
private final AuthorizableConfigBean authorizableConfigBean;
91-
92-
private PrecreatedExternalGroup(AuthorizableConfigBean authorizableConfigBean) {
93-
this.authorizableConfigBean = authorizableConfigBean;
97+
private final String id;
98+
private final String externalId;
99+
private final String path;
100+
101+
private PrecreatedExternalGroup(String id, String externalId, String path) {
102+
this.id = id;
103+
this.externalId = externalId;
104+
this.path = path;
94105
}
95106

96107
@Override
97108
public String getId() {
98-
return authorizableConfigBean.getAuthorizableId();
109+
return id;
99110
}
100111

101112
@Override
102113
public String getPrincipalName() {
103-
String principalName = ExternalIdentityRef.fromString(authorizableConfigBean.getExternalId()).getId();
104-
return principalName;
114+
return getExternalId().getId();
105115
}
106116

107117
@Override
108118
public String getIntermediatePath() {
109-
String rawIntermediatePath = authorizableConfigBean.getPath();
119+
String rawIntermediatePath = path;
110120
String intermediatePath = StringUtils.removeStart(rawIntermediatePath, Constants.GROUPS_ROOT + "/");
111121
return intermediatePath;
112122
}
113123

114124
@Override
115125
public ExternalIdentityRef getExternalId() {
116-
return ExternalIdentityRef.fromString(authorizableConfigBean.getExternalId());
126+
return ExternalIdentityRef.fromString(externalId);
117127
}
118128

119129
@Override

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AuthorizableConfigBean.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import java.util.ArrayList;
1717
import java.util.Arrays;
18+
import java.util.HashMap;
1819
import java.util.List;
1920
import java.util.Map;
2021
import java.util.regex.Pattern;
@@ -361,4 +362,15 @@ public void setExternalSync(boolean externalSync) {
361362
this.externalSync = externalSync;
362363
}
363364

365+
public Map<String, Object> getVariablesForInterpolation() {
366+
Map<String,Object> vars = new HashMap<>();
367+
Map<String,String> groupVar = new HashMap<>();
368+
String groupId = getAuthorizableId();
369+
groupVar.put("id", groupId);
370+
groupVar.put("name", StringUtils.defaultIfEmpty(getName(), groupId));
371+
groupVar.put("path", getPath());
372+
vars.put("group", groupVar);
373+
return vars;
374+
}
375+
364376
}

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreator.java

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ void createTestUserConfigs(AcConfiguration acConfiguration, InstallationLogger l
7676
Matcher matcher = pattern.matcher(groupId);
7777
if (matcher.matches()) {
7878

79-
Map<String, Object> vars = getVarsForAuthConfigBean(groupAuthConfigBean);
79+
Map<String, Object> vars = groupAuthConfigBean.getVariablesForInterpolation();
8080
// also add all captured groups from the matcher as variables
8181
vars.putAll(getVarsForCapturedGroups(matcher));
8282

@@ -133,27 +133,11 @@ Map<String, Object> getVarsForCapturedGroups(Matcher matcher) {
133133
return vars;
134134
}
135135

136-
Map<String, Object> getVarsForAuthConfigBean(AuthorizableConfigBean groupAuthConfigBean) {
137-
Map<String,Object> vars = new HashMap<>();
138-
Map<String,String> groupVar = new HashMap<>();
139-
String groupId = groupAuthConfigBean.getAuthorizableId();
140-
groupVar.put("id", groupId);
141-
groupVar.put("name", StringUtils.defaultIfEmpty(groupAuthConfigBean.getName(), groupId));
142-
groupVar.put("path", groupAuthConfigBean.getPath());
143-
vars.put("group", groupVar);
144-
return vars;
145-
}
146-
147136
String processValue(String value, Map<? extends Object, ? extends Object> variables) {
148-
149-
String elWithDollarExpressions = value.replaceAll("%\\{([^\\}]+)\\}", "\\${$1}");
150137
if(elEvaluator==null) {
151138
elEvaluator = new YamlMacroElEvaluator();
152139
}
153-
154-
String interpolatedValue = elEvaluator.evaluateEl(elWithDollarExpressions, String.class, variables);
155-
156-
return interpolatedValue;
140+
return elEvaluator.evaluateElWithPercentSyntax(value, String.class, variables);
157141
}
158142

159143

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroElEvaluator.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.commons.lang3.StringUtils;
4040
import org.apache.el.ExpressionFactoryImpl;
4141

42+
4243
/** Evaluates expressions that may contain variables from for loops.
4344
*
4445
* Not an OSGi Service as it carries state and is not multi-threading safe.
@@ -101,6 +102,19 @@ public Object convertToType(Object obj, Class<?> type) {
101102
};
102103
}
103104

105+
/**
106+
* Similar to {@link #evaluateEl(String, Class, Map)} but evaluates expressions that use the percent syntax <code>%{...}</code> instead of <code>${...}</code>.
107+
* @param <T>
108+
* @param value
109+
* @param expectedResultType
110+
* @param variables
111+
* @return the interpolated value
112+
*/
113+
public <T> T evaluateElWithPercentSyntax(String value, Class<T> expectedResultType, Map<? extends Object, ? extends Object> variables) {
114+
String elWithDollarExpressions = value.replaceAll("%\\{([^\\}]+)\\}", "\\${$1}");
115+
return evaluateEl(elWithDollarExpressions, expectedResultType, variables);
116+
}
117+
104118
public <T> T evaluateEl(String el, Class<T> expectedResultType, Map<? extends Object, ? extends Object> variables) {
105119

106120
vars = variables;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package biz.netcentric.cq.tools.actool.authorizableinstaller.impl;
2+
3+
/*-
4+
* #%L
5+
* Access Control Tool Bundle
6+
* %%
7+
* Copyright (C) 2015 - 2025 Cognizant Netcentric
8+
* %%
9+
* All rights reserved. This program and the accompanying materials
10+
* are made available under the terms of the Eclipse Public License v1.0
11+
* which accompanies this distribution, and is available at
12+
* http://www.eclipse.org/legal/epl-v10.html
13+
* #L%
14+
*/
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import org.junit.jupiter.api.Test;
18+
import biz.netcentric.cq.tools.actool.configmodel.AuthorizableConfigBean;
19+
20+
class ExternalGroupInstallerServiceImplTest {
21+
22+
@Test
23+
void testGetExternalId() {
24+
AuthorizableConfigBean groupAuthConfigBean = new AuthorizableConfigBean();
25+
groupAuthConfigBean.setAuthorizableId("test-group-id");
26+
groupAuthConfigBean.setName("my-name");
27+
groupAuthConfigBean.setPath("/path/to/group");
28+
groupAuthConfigBean.setExternalId("%{group.id};ims");
29+
assertEquals("test-group-id;ims", ExternalGroupInstallerServiceImpl.getExternalId(groupAuthConfigBean));
30+
}
31+
32+
}

accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreatorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private Map<String, Object> getTestVars(String name, Matcher matcher) {
8888
groupAuthConfigBean.setAuthorizableId(TEST_GROUP_ID);
8989
groupAuthConfigBean.setName(name);
9090
groupAuthConfigBean.setPath(TEST_GROUP_PATH);
91-
Map<String, Object> vars = new HashMap<>(testUserConfigsCreator.getVarsForAuthConfigBean(groupAuthConfigBean));
91+
Map<String, Object> vars = new HashMap<>(groupAuthConfigBean.getVariablesForInterpolation());
9292
vars.putAll(testUserConfigsCreator.getVarsForCapturedGroups(matcher));
9393
return vars;
9494
}

docs/AdvancedFeatures.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -288,18 +288,14 @@ property | comment | required
288288
--- | --- | ---
289289
`createForGroupNamesRegEx` | A regex (matched against authorizableId of groups) to select the groups, test users should be created for. The regular expression may contain [capturing groups](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html#cg). | required
290290
`prefix` | The prefix for the authorizable id, for instance if prefix "tu-" is given, a user "tu-myproject-editors" will be created for group "myproject-editors" | required
291-
`name` | The name as configured in user's profile, allows for interpolation with EL *) | optional, defaults to "Test User %{group.name}"
291+
`name` | The name as configured in user's profile, allows for interpolation with EL[^1] | optional, defaults to "Test User %{group.name}"
292292
`description` | The description as configured in user's profile, allows for interpolation with EL *) | optional, not set by default
293-
`email` | The email as configured in user's profile, allows for interpolation with EL *). | optional, not set by default
293+
`email` | The email as configured in user's profile, allows for interpolation with EL[^1]. | optional, not set by default
294294
`path` | The location where the test users shall be created | required
295295
`password` | The password for all test users to be created. Can be encrypted using CryptoSupport. Defaults simply to the authorizable id of the test user. Allows for interpolation with EL *) | optional
296296
`skipForRunmodes` | The configuration is placed in a regular config file, hence it is possible to add one to an author configuration (located in e.g. in a folder "config.author" and one to a publish configuration (e.g. folder "config.publish"). To avoid creating special runmodes folders just for this configuration that list all runmodes except production, skipForRunmodes can be a comma-separated list of runmodes, where the users are not created. Defaults to prod,production | optional
297297
`impersonationAllowedFor` | List of users that can impersonate auto-created test users | optional
298298

299-
*) Interpolation of group properties can be used with EL, however as `$` is evaluated at an earlier stage, `%{}` is used here. Available is `%{group.id}`, `%{group.name}`, `%{group.path}` or expressions like `%{split(group.path,'/')[2]}`.
300-
301-
The special variables `%{cg<capturingGroupIndex>}` may be used to reference a capturing group from the regular expression given in `createForGroupNamesRegEx` matched against the group id. The variable `%{cg0}` stands for the complete group id (since version 3.2.0).
302-
303299
Example:
304300

305301
```
@@ -482,3 +478,4 @@ This example gives the group `myproj-editor` edit rights for all content in fold
482478

483479

484480
[felix-interpolation-plugin]: https://github.com/apache/felix-dev/blob/master/configadmin-plugins/interpolation/README.md
481+
[^1]: Interpolation of group properties can be used with EL, however as `$` is evaluated at an earlier stage, `%{}` is used here. Available is `%{group.id}`, `%{group.name}`, `%{group.path}` or expressions like `%{split(group.path,'/')[2]}`. The special variables `%{cg<capturingGroupIndex>}` may be used to reference a capturing group from the regular expression given in `createForGroupNamesRegEx` matched against the group id. The variable `%{cg0}` stands for the complete group id (since version 3.2.0).

docs/Configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ property | comment | required
9696
--- | --- | ---
9797
name | Name of the group as shown in UI. Sets the property `profile/givenName` of that group. | optional
9898
description | Description of the group | optional
99-
externalId | Required for groups which are synchronized from [external sources](https://jackrabbit.apache.org/oak/docs/security/authentication/externalloginmodule.html) like [LDAP](https://jackrabbit.apache.org/oak/docs/security/authentication/ldap.html) or [Adobe IMS](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/security/ims-support#aem-configuration). This establishes a connection between an (internal) JCR group and an externally managed group (and is persisted in the group's node in the property `rep:externalId`). The value has to be in format `<external-id>;<provider-name>`. How the external ID and provider name look like is *External Identity Provider dependent*: For **Adobe IMS** it usually is `<groupId>;ims` while for **Oak LDAP** it usually is `<LDAP-DN>;<IDP-NAME>` where LDAP-DN is the full distinguished name and IDP-NAME is configured in OSGI config PID `org.apache.jackrabbit.oak.security.authentication.ldap.impl.LdapIdentityProvider` property `provider-name`. LDAP Example: `externalId: "cn=group-name,ou=mydepart,ou=Groups,dc=comp,dc=com;IDPNAME"`. Make sure to also set the group id according to how it is extracted by the external identify provider (configurable via OSGi configuration of the external identity provider). Using groups being synced from external sources in `isMemberOf` will cause an error to avoid problems with [dynamic memberships](https://jackrabbit.apache.org/oak/docs/security/authentication/external/defaultusersync.html#protecting-synchronized-external-users-groups). Use `allowExternalGroupsInIsMemberOf: true` in `global_config` if you need to override this behaviour (should be used rarely). Since v1.9.3 | optional
99+
externalId | Required for groups which are synchronized from [external sources](https://jackrabbit.apache.org/oak/docs/security/authentication/externalloginmodule.html) like [LDAP](https://jackrabbit.apache.org/oak/docs/security/authentication/ldap.html) or [Adobe IMS](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/security/ims-support#aem-configuration). This establishes a connection between an (internal) JCR group and an externally managed group (and is persisted in the group's node in the property `rep:externalId`). The value has to be in format `<external-id>;<provider-name>`. How the external ID and provider name look like is *External Identity Provider dependent*: For **Adobe IMS** it usually is `<groupId>;ims` while for **Oak LDAP** it usually is `<LDAP-DN>;<IDP-NAME>` where LDAP-DN is the full distinguished name and IDP-NAME is configured in OSGI config PID `org.apache.jackrabbit.oak.security.authentication.ldap.impl.LdapIdentityProvider` property `provider-name`. LDAP Example: `externalId: "cn=group-name,ou=mydepart,ou=Groups,dc=comp,dc=com;IDPNAME"`. Make sure to also set the group id according to how it is extracted by the external identify provider (configurable via OSGi configuration of the external identity provider). Using groups being synced from external sources in `isMemberOf` will cause an error to avoid problems with [dynamic memberships](https://jackrabbit.apache.org/oak/docs/security/authentication/external/defaultusersync.html#protecting-synchronized-external-users-groups). Use `allowExternalGroupsInIsMemberOf: true` in `global_config` if you need to override this behaviour (should be used rarely). Since v1.9.3. Allows for interpolation with EL[^1] since 3.6.2. | optional
100100
path | Path of the intermediate node either relative or absolute. If relative, `/home/groups` is automatically prefixed. By default some implementation specific path is choosen. Usually the full group path is the (intermediate) path concatenated with a [randomized authorizable id](https://jackrabbit.apache.org/oak/docs/apidocs/org/apache/jackrabbit/oak/security/user/RandomAuthorizableNodeName.html). | optional
101101
isMemberOf | List of groups this groups is a member of. May be provided as yaml list or as comma-separated yaml string (*the use of comma-separated yaml strings is deprecated*, available to remain backwards compatible). | optional
102102
memberOf | Same meaning as `isMemberOf`. This property is *deprecated*, please use `isMemberOf` instead. | optional
@@ -345,3 +345,4 @@ If issues occur during the application of the configurations in CRX the installa
345345

346346

347347
[bouncycastle]: https://www.bouncycastle.org/
348+
[^1]: Interpolation of group properties can be used with EL, however as `$` is evaluated at an earlier stage, `%{}` is used here. Available is `%{group.id}`, `%{group.name}`, `%{group.path}` or expressions like `%{split(group.path,'/')[2]}`.

0 commit comments

Comments
 (0)