From 2e47e2643366f826fbc3e8fc5379412508f5a3bb Mon Sep 17 00:00:00 2001 From: Dominik Przybyl Date: Sun, 25 Oct 2026 21:16:07 +0100 Subject: [PATCH] Allow protected passwords in ACL operations Users can now pass CryptoSupport-protected passwords (`{...}` form) directly to acl.createUser, acl.setPassword and acl.check.password without unprotecting them from the script. Previously the encrypted string was stored verbatim, so the alternative was decrypting ahead of time and hardcoding the plaintext. CodeContext resolves CryptoSupport through osgiContext.getService (same pattern as NotificationManager) and passes it into Acl -> AclContext -> AuthorizableManager. A small CryptoDecryptor wraps the service and is applied on every password that flows through createUser, changePassword and testPassword; blank or unprotected values pass through unchanged, and CryptoException is surfaced as AclException. The granite-crypto API is already on the classpath transitively via cq-replication, so no new dependency is needed. The set_password and create_user snippets explain in their tooltip that only the password argument is auto-decrypted. A new osgi_crypto_protect snippet shows how to generate a {...} value via cryptoSupport.protect(), which works on any AEM including AEMaaCS where /system/console/crypto is not reachable. Closes #335 --- .../java/dev/vml/es/acm/core/acl/Acl.java | 9 +-- .../dev/vml/es/acm/core/acl/AclContext.java | 12 ++-- .../core/acl/utils/AuthorizableManager.java | 20 ++++++- .../acm/core/acl/utils/CryptoDecryptor.java | 26 +++++++++ .../dev/vml/es/acm/core/code/CodeContext.java | 3 +- .../core/acl/utils/CryptoDecryptorTest.java | 55 +++++++++++++++++++ .../available/core/acl/create_user.yml | 3 +- .../available/core/acl/set_password.yml | 6 +- .../available/core/osgi/crypto_protect.yml | 6 ++ 9 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 core/src/main/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptor.java create mode 100644 core/src/test/java/dev/vml/es/acm/core/acl/utils/CryptoDecryptorTest.java create mode 100644 ui.content/src/main/content/jcr_root/conf/acm/settings/snippet/available/core/osgi/crypto_protect.yml 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.