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.