diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImpl.java index 7ec9343b2..2bac8ecc3 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImpl.java @@ -37,6 +37,7 @@ import biz.netcentric.cq.tools.actool.authorizableinstaller.AuthorizableCreatorException; import biz.netcentric.cq.tools.actool.configmodel.AuthorizableConfigBean; +import biz.netcentric.cq.tools.actool.configreader.YamlMacroElEvaluator; import biz.netcentric.cq.tools.actool.helper.Constants; import biz.netcentric.cq.tools.actool.history.InstallationLogger; @@ -59,11 +60,8 @@ public Authorizable createGroupWithExternalId( throws AuthorizableExistsException, RepositoryException, AuthorizableCreatorException { - if (StringUtils.isBlank(authorizableConfigBean.getExternalId())) { - throw new IllegalStateException("externalId must not be empty for " + authorizableConfigBean); - } - - ExternalGroup externalGroup = new PrecreatedExternalGroup(authorizableConfigBean); + String externalId = getExternalId(authorizableConfigBean); + ExternalGroup externalGroup = new PrecreatedExternalGroup(authorizableConfigBean.getAuthorizableId(), externalId, authorizableConfigBean.getPath()); ExternalGroupPrecreatorSyncContext externalGroupPrecreatorSyncContext = new ExternalGroupPrecreatorSyncContext(userManager, session.getValueFactory()); @@ -72,6 +70,15 @@ public Authorizable createGroupWithExternalId( return group; } + static String getExternalId(final AuthorizableConfigBean authorizableConfigBean) { + YamlMacroElEvaluator elEvaluator = new YamlMacroElEvaluator(); + String externalId = elEvaluator.evaluateElWithPercentSyntax(authorizableConfigBean.getExternalId(), String.class, authorizableConfigBean.getVariablesForInterpolation()); + if (StringUtils.isBlank(externalId)) { + throw new IllegalStateException("externalId must not be empty for " + authorizableConfigBean); + } + return externalId; + } + // simple workaround to make protected method available here private final class ExternalGroupPrecreatorSyncContext extends DefaultSyncContext { @@ -87,33 +94,36 @@ private Group createExternalGroup(ExternalGroup eg) throws RepositoryException { // mapping AuthorizableConfigBean -> ExternalGroup private final class PrecreatedExternalGroup implements ExternalGroup { - private final AuthorizableConfigBean authorizableConfigBean; - - private PrecreatedExternalGroup(AuthorizableConfigBean authorizableConfigBean) { - this.authorizableConfigBean = authorizableConfigBean; + private final String id; + private final String externalId; + private final String path; + + private PrecreatedExternalGroup(String id, String externalId, String path) { + this.id = id; + this.externalId = externalId; + this.path = path; } @Override public String getId() { - return authorizableConfigBean.getAuthorizableId(); + return id; } @Override public String getPrincipalName() { - String principalName = ExternalIdentityRef.fromString(authorizableConfigBean.getExternalId()).getId(); - return principalName; + return getExternalId().getId(); } @Override public String getIntermediatePath() { - String rawIntermediatePath = authorizableConfigBean.getPath(); + String rawIntermediatePath = path; String intermediatePath = StringUtils.removeStart(rawIntermediatePath, Constants.GROUPS_ROOT + "/"); return intermediatePath; } @Override public ExternalIdentityRef getExternalId() { - return ExternalIdentityRef.fromString(authorizableConfigBean.getExternalId()); + return ExternalIdentityRef.fromString(externalId); } @Override diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AuthorizableConfigBean.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AuthorizableConfigBean.java index 67e683598..8a6d03fe6 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AuthorizableConfigBean.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AuthorizableConfigBean.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -361,4 +362,15 @@ public void setExternalSync(boolean externalSync) { this.externalSync = externalSync; } + public Map getVariablesForInterpolation() { + Map vars = new HashMap<>(); + Map groupVar = new HashMap<>(); + String groupId = getAuthorizableId(); + groupVar.put("id", groupId); + groupVar.put("name", StringUtils.defaultIfEmpty(getName(), groupId)); + groupVar.put("path", getPath()); + vars.put("group", groupVar); + return vars; + } + } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreator.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreator.java index f18d7b6c1..b6fc8220f 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreator.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreator.java @@ -76,7 +76,7 @@ void createTestUserConfigs(AcConfiguration acConfiguration, InstallationLogger l Matcher matcher = pattern.matcher(groupId); if (matcher.matches()) { - Map vars = getVarsForAuthConfigBean(groupAuthConfigBean); + Map vars = groupAuthConfigBean.getVariablesForInterpolation(); // also add all captured groups from the matcher as variables vars.putAll(getVarsForCapturedGroups(matcher)); @@ -133,27 +133,11 @@ Map getVarsForCapturedGroups(Matcher matcher) { return vars; } - Map getVarsForAuthConfigBean(AuthorizableConfigBean groupAuthConfigBean) { - Map vars = new HashMap<>(); - Map groupVar = new HashMap<>(); - String groupId = groupAuthConfigBean.getAuthorizableId(); - groupVar.put("id", groupId); - groupVar.put("name", StringUtils.defaultIfEmpty(groupAuthConfigBean.getName(), groupId)); - groupVar.put("path", groupAuthConfigBean.getPath()); - vars.put("group", groupVar); - return vars; - } - String processValue(String value, Map variables) { - - String elWithDollarExpressions = value.replaceAll("%\\{([^\\}]+)\\}", "\\${$1}"); if(elEvaluator==null) { elEvaluator = new YamlMacroElEvaluator(); } - - String interpolatedValue = elEvaluator.evaluateEl(elWithDollarExpressions, String.class, variables); - - return interpolatedValue; + return elEvaluator.evaluateElWithPercentSyntax(value, String.class, variables); } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroElEvaluator.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroElEvaluator.java index 9f58ca00e..6ada205de 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroElEvaluator.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroElEvaluator.java @@ -39,6 +39,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.el.ExpressionFactoryImpl; + /** Evaluates expressions that may contain variables from for loops. * * 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) { }; } + /** + * Similar to {@link #evaluateEl(String, Class, Map)} but evaluates expressions that use the percent syntax %{...} instead of ${...}. + * @param + * @param value + * @param expectedResultType + * @param variables + * @return the interpolated value + */ + public T evaluateElWithPercentSyntax(String value, Class expectedResultType, Map variables) { + String elWithDollarExpressions = value.replaceAll("%\\{([^\\}]+)\\}", "\\${$1}"); + return evaluateEl(elWithDollarExpressions, expectedResultType, variables); + } + public T evaluateEl(String el, Class expectedResultType, Map variables) { vars = variables; diff --git a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImplTest.java b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImplTest.java new file mode 100644 index 000000000..281b7ff34 --- /dev/null +++ b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/ExternalGroupInstallerServiceImplTest.java @@ -0,0 +1,32 @@ +package biz.netcentric.cq.tools.actool.authorizableinstaller.impl; + +/*- + * #%L + * Access Control Tool Bundle + * %% + * Copyright (C) 2015 - 2025 Cognizant Netcentric + * %% + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import biz.netcentric.cq.tools.actool.configmodel.AuthorizableConfigBean; + +class ExternalGroupInstallerServiceImplTest { + + @Test + void testGetExternalId() { + AuthorizableConfigBean groupAuthConfigBean = new AuthorizableConfigBean(); + groupAuthConfigBean.setAuthorizableId("test-group-id"); + groupAuthConfigBean.setName("my-name"); + groupAuthConfigBean.setPath("/path/to/group"); + groupAuthConfigBean.setExternalId("%{group.id};ims"); + assertEquals("test-group-id;ims", ExternalGroupInstallerServiceImpl.getExternalId(groupAuthConfigBean)); + } + +} diff --git a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreatorTest.java b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreatorTest.java index 1228a14c0..441399368 100644 --- a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreatorTest.java +++ b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configreader/TestUserConfigsCreatorTest.java @@ -88,7 +88,7 @@ private Map getTestVars(String name, Matcher matcher) { groupAuthConfigBean.setAuthorizableId(TEST_GROUP_ID); groupAuthConfigBean.setName(name); groupAuthConfigBean.setPath(TEST_GROUP_PATH); - Map vars = new HashMap<>(testUserConfigsCreator.getVarsForAuthConfigBean(groupAuthConfigBean)); + Map vars = new HashMap<>(groupAuthConfigBean.getVariablesForInterpolation()); vars.putAll(testUserConfigsCreator.getVarsForCapturedGroups(matcher)); return vars; } diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md index e10236e68..324103462 100644 --- a/docs/AdvancedFeatures.md +++ b/docs/AdvancedFeatures.md @@ -288,18 +288,14 @@ property | comment | required --- | --- | --- `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 `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 -`name` | The name as configured in user's profile, allows for interpolation with EL *) | optional, defaults to "Test User %{group.name}" +`name` | The name as configured in user's profile, allows for interpolation with EL[^1] | optional, defaults to "Test User %{group.name}" `description` | The description as configured in user's profile, allows for interpolation with EL *) | optional, not set by default -`email` | The email as configured in user's profile, allows for interpolation with EL *). | optional, not set by default +`email` | The email as configured in user's profile, allows for interpolation with EL[^1]. | optional, not set by default `path` | The location where the test users shall be created | required `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 `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 `impersonationAllowedFor` | List of users that can impersonate auto-created test users | optional -*) 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}` 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). - Example: ``` @@ -482,3 +478,4 @@ This example gives the group `myproj-editor` edit rights for all content in fold [felix-interpolation-plugin]: https://github.com/apache/felix-dev/blob/master/configadmin-plugins/interpolation/README.md +[^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}` 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). diff --git a/docs/Configuration.md b/docs/Configuration.md index 7b656e88e..2baf8fee1 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -96,7 +96,7 @@ property | comment | required --- | --- | --- name | Name of the group as shown in UI. Sets the property `profile/givenName` of that group. | optional description | Description of the group | optional -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 `;`. How the external ID and provider name look like is *External Identity Provider dependent*: For **Adobe IMS** it usually is `;ims` while for **Oak LDAP** it usually is `;` 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 +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 `;`. How the external ID and provider name look like is *External Identity Provider dependent*: For **Adobe IMS** it usually is `;ims` while for **Oak LDAP** it usually is `;` 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 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 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 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 [bouncycastle]: https://www.bouncycastle.org/ +[^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]}`.