diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java
index c3f8563f9..53a325a8c 100644
--- a/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java
+++ b/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java
@@ -1,5 +1,6 @@
package dev.vml.es.acm.core.acl;
+import com.adobe.granite.crypto.CryptoSupport;
import dev.vml.es.acm.core.acl.authorizable.AclAuthorizable;
import dev.vml.es.acm.core.acl.authorizable.AclGroup;
import dev.vml.es.acm.core.acl.authorizable.AclUser;
@@ -17,12 +18,8 @@ public class Acl {
private final AclChecker checker;
- public Acl(ResourceResolver resourceResolver) {
- this(resourceResolver, CommitPolicy.of(resourceResolver, true));
- }
-
- public Acl(ResourceResolver resourceResolver, CommitPolicy commitPolicy) {
- this.context = new AclContext(resourceResolver, commitPolicy);
+ public Acl(ResourceResolver resourceResolver, CommitPolicy commitPolicy, CryptoSupport cryptoSupport) {
+ this.context = new AclContext(resourceResolver, commitPolicy, cryptoSupport);
this.checker = new AclChecker(context);
}
diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java
index b8e0b08ac..5c998d8b8 100644
--- a/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java
+++ b/core/src/main/java/dev/vml/es/acm/core/acl/AclContext.java
@@ -1,9 +1,11 @@
package dev.vml.es.acm.core.acl;
+import com.adobe.granite.crypto.CryptoSupport;
import dev.vml.es.acm.core.acl.authorizable.AclAuthorizable;
import dev.vml.es.acm.core.acl.authorizable.AclGroup;
import dev.vml.es.acm.core.acl.authorizable.AclUser;
import dev.vml.es.acm.core.acl.utils.AuthorizableManager;
+import dev.vml.es.acm.core.acl.utils.CryptoDecryptor;
import dev.vml.es.acm.core.acl.utils.PermissionsManager;
import dev.vml.es.acm.core.repo.CommitPolicy;
import dev.vml.es.acm.core.repo.Repo;
@@ -33,18 +35,16 @@ public class AclContext {
private final PermissionsManager permissionsManager;
- public AclContext(ResourceResolver resourceResolver) {
- this(resourceResolver, CommitPolicy.of(resourceResolver, true));
- }
-
- public AclContext(ResourceResolver resourceResolver, CommitPolicy commitPolicy) {
+ public AclContext(ResourceResolver resourceResolver, CommitPolicy commitPolicy, CryptoSupport cryptoSupport) {
try {
JackrabbitSession session = (JackrabbitSession) resourceResolver.adaptTo(Session.class);
UserManager userManager = session.getUserManager();
AccessControlManager accessControlManager = session.getAccessControlManager();
ValueFactory valueFactory = session.getValueFactory();
+ CryptoDecryptor cryptoDecryptor = new CryptoDecryptor(cryptoSupport);
this.resourceResolver = resourceResolver;
- this.authorizableManager = new AuthorizableManager(session, userManager, valueFactory, commitPolicy);
+ this.authorizableManager =
+ new AuthorizableManager(session, userManager, valueFactory, commitPolicy, cryptoDecryptor);
this.permissionsManager = new PermissionsManager(session, accessControlManager, valueFactory, commitPolicy);
} catch (RepositoryException e) {
throw new AclException("Cannot access repository while obtaining ACL context!", e);
diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java b/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java
index ffb24db5b..2885103b1 100644
--- a/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java
+++ b/core/src/main/java/dev/vml/es/acm/core/acl/utils/AuthorizableManager.java
@@ -30,12 +30,19 @@ public class AuthorizableManager {
private final CommitPolicy commitPolicy;
+ private final CryptoDecryptor cryptoDecryptor;
+
public AuthorizableManager(
- JackrabbitSession session, UserManager userManager, ValueFactory valueFactory, CommitPolicy commitPolicy) {
+ JackrabbitSession session,
+ UserManager userManager,
+ ValueFactory valueFactory,
+ CommitPolicy commitPolicy,
+ CryptoDecryptor cryptoDecryptor) {
this.session = session;
this.userManager = userManager;
this.valueFactory = valueFactory;
this.commitPolicy = commitPolicy;
+ this.cryptoDecryptor = cryptoDecryptor;
}
public User createUser(String id, String password, String path) {
@@ -43,6 +50,8 @@ public User createUser(String id, String password, String path) {
Principal principal = () -> id;
if (StringUtils.isEmpty(password)) {
password = PasswordUtils.generateRandomPassword();
+ } else {
+ password = decrypt(password);
}
User user = userManager.createUser(id, password, principal, path);
save(String.format("creating user '%s'", id));
@@ -166,8 +175,9 @@ public boolean testPassword(User user, String password) {
String userId = null;
try {
userId = user.getID();
+ String plainPassword = decrypt(password);
Repository repository = session.getRepository();
- Credentials credentials = new SimpleCredentials(userId, password.toCharArray());
+ Credentials credentials = new SimpleCredentials(userId, plainPassword.toCharArray());
repository.login(credentials).logout();
return true;
} catch (LoginException e) {
@@ -181,7 +191,7 @@ public void changePassword(User user, String password) {
String userId = null;
try {
userId = user.getID();
- user.changePassword(password);
+ user.changePassword(decrypt(password));
save(String.format("changing password for user '%s'", userId));
} catch (RepositoryException e) {
throw new AclException(String.format("Failed to set password for user '%s'", userId), e);
@@ -236,4 +246,8 @@ public boolean removeProperty(Authorizable authorizable, String relPath) {
private void save(String context) {
commitPolicy.commit(context);
}
+
+ private String decrypt(String text) {
+ return cryptoDecryptor != null ? cryptoDecryptor.decrypt(text) : text;
+ }
}
diff --git a/core/src/main/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptor.java b/core/src/main/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptor.java
new file mode 100644
index 000000000..d7e9e4271
--- /dev/null
+++ b/core/src/main/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptor.java
@@ -0,0 +1,26 @@
+package dev.vml.es.acm.core.acl.utils;
+
+import com.adobe.granite.crypto.CryptoException;
+import com.adobe.granite.crypto.CryptoSupport;
+import dev.vml.es.acm.core.acl.AclException;
+import org.apache.commons.lang3.StringUtils;
+
+public class CryptoDecryptor {
+
+ private final CryptoSupport cryptoSupport;
+
+ public CryptoDecryptor(CryptoSupport cryptoSupport) {
+ this.cryptoSupport = cryptoSupport;
+ }
+
+ public String decrypt(String text) {
+ if (cryptoSupport == null || StringUtils.isBlank(text) || !cryptoSupport.isProtected(text)) {
+ return text;
+ }
+ try {
+ return cryptoSupport.unprotect(text);
+ } catch (CryptoException e) {
+ throw new AclException("Failed to decrypt text", e);
+ }
+ }
+}
diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java
index f0212f3b5..4068b4718 100644
--- a/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java
+++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeContext.java
@@ -1,5 +1,6 @@
package dev.vml.es.acm.core.code;
+import com.adobe.granite.crypto.CryptoSupport;
import dev.vml.es.acm.core.acl.Acl;
import dev.vml.es.acm.core.code.script.ExtensionScript;
import dev.vml.es.acm.core.format.Formatter;
@@ -46,7 +47,7 @@ public CodeContext(OsgiContext osgiContext, ResourceResolver resourceResolver) {
this.log = LoggerFactory.getLogger(getClass());
this.repo = new Repo(resourceResolver);
- this.acl = new Acl(resourceResolver, repo);
+ this.acl = new Acl(resourceResolver, repo, osgiContext.getService(CryptoSupport.class));
this.activator = new Activator(resourceResolver, osgiContext);
this.formatter = new Formatter();
this.notifier = osgiContext.getService(NotificationManager.class);
diff --git a/core/src/test/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptorTest.java b/core/src/test/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptorTest.java
new file mode 100644
index 000000000..66c4e9b9e
--- /dev/null
+++ b/core/src/test/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptorTest.java
@@ -0,0 +1,55 @@
+package dev.vml.es.acm.core.acl.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+import com.adobe.granite.crypto.CryptoException;
+import com.adobe.granite.crypto.CryptoSupport;
+import dev.vml.es.acm.core.acl.AclException;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class CryptoDecryptorTest {
+
+ @Test
+ void returnsInputWhenCryptoSupportMissing() {
+ CryptoDecryptor decryptor = new CryptoDecryptor(null);
+ assertEquals("{abc}", decryptor.decrypt("{abc}"));
+ }
+
+ @Test
+ void returnsInputWhenTextIsBlank() {
+ CryptoDecryptor decryptor = new CryptoDecryptor(Mockito.mock(CryptoSupport.class));
+ assertNull(decryptor.decrypt(null));
+ assertEquals("", decryptor.decrypt(""));
+ assertEquals(" ", decryptor.decrypt(" "));
+ }
+
+ @Test
+ void returnsInputWhenNotProtected() throws CryptoException {
+ CryptoSupport cryptoSupport = Mockito.mock(CryptoSupport.class);
+ when(cryptoSupport.isProtected("plainText")).thenReturn(false);
+ assertEquals("plainText", new CryptoDecryptor(cryptoSupport).decrypt("plainText"));
+ }
+
+ @Test
+ void unprotectsEncryptedText() throws CryptoException {
+ CryptoSupport cryptoSupport = Mockito.mock(CryptoSupport.class);
+ when(cryptoSupport.isProtected("{encrypted}")).thenReturn(true);
+ when(cryptoSupport.unprotect("{encrypted}")).thenReturn("decrypted");
+ assertEquals("decrypted", new CryptoDecryptor(cryptoSupport).decrypt("{encrypted}"));
+ }
+
+ @Test
+ void wrapsFailureInAclException() throws CryptoException {
+ CryptoSupport cryptoSupport = Mockito.mock(CryptoSupport.class);
+ when(cryptoSupport.isProtected("{broken}")).thenReturn(true);
+ doThrow(new CryptoException("hmac mismatch")).when(cryptoSupport).unprotect("{broken}");
+ AclException exception =
+ assertThrows(AclException.class, () -> new CryptoDecryptor(cryptoSupport).decrypt("{broken}"));
+ assertEquals("Failed to decrypt text", exception.getMessage());
+ }
+}
diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/create_user.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/create_user.yml
index ece828589..9bc3e0dab 100644
--- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/create_user.yml
+++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/create_user.yml
@@ -4,4 +4,5 @@ content: |
acl.createUser { id = "${1:id}"; fullName = "${2:fullName}"; password = "${3:password}"; email = "${4:email}" }
documentation: |
Create or retrieve the user with the given ID, full name, password and email.
- The user will only be created if it does not already exist.
+ The user will only be created if it does not already exist.
+ Only the password argument is automatically decrypted when wrapped in {...} using CryptoSupport. The encrypted value can be generated via /system/console/crypto (local/on-prem) or cryptoSupport.protect(text) (any AEM — see snippet osgi_crypto_protect). All other arguments remain unchanged.
diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/set_password.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/set_password.yml
index 453440198..3fe7f3a11 100644
--- a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/set_password.yml
+++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/acl/set_password.yml
@@ -1,7 +1,7 @@
group: ACL
-name: acl_set
- password
+name: acl_set_password
content: |
acl.setPassword { userId = "${1:userId}"; password = "${2:password}" }
documentation: |
- Set the password for the given user.
+ Set the password for the given user.
+ Only the password argument is automatically decrypted when wrapped in {...} using CryptoSupport. The encrypted value can be generated via /system/console/crypto (local/on-prem) or cryptoSupport.protect(text) (any AEM — see snippet osgi_crypto_protect). All other arguments remain unchanged.
diff --git a/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/osgi/crypto_protect.yml b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/osgi/crypto_protect.yml
new file mode 100644
index 000000000..b4a9160de
--- /dev/null
+++ b/ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/osgi/crypto_protect.yml
@@ -0,0 +1,6 @@
+group: OSGi
+name: osgi_crypto_protect
+content: |
+ osgi.getService(com.adobe.granite.crypto.CryptoSupport.class).protect("${1:plainText}")
+documentation: |
+ Encrypt text with CryptoSupport and get the {...} value accepted by acl.createUser and acl.setPassword.