From 47cbecf1a96f4576aaebd5a628f94018139cb102 Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Sun, 2 Feb 2025 10:51:33 +0100 Subject: [PATCH] Master source lookup dispatcher (#93) This closes #90 --------- Co-authored-by: Tamas Cservenak --- .../MasterSourceLookupDispatcher.java | 94 +++++++++++++++++++ .../internal/DefaultSecDispatcherTest.java | 30 +++++- .../MasterSourceLookupDispatcherTest.java | 69 ++++++++++++++ 3 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcher.java create mode 100644 src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcherTest.java diff --git a/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcher.java b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcher.java new file mode 100644 index 0000000..d47cfaa --- /dev/null +++ b/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcher.java @@ -0,0 +1,94 @@ +/* + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.codehaus.plexus.components.secdispatcher.internal.dispatchers; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.codehaus.plexus.components.secdispatcher.Dispatcher; +import org.codehaus.plexus.components.secdispatcher.DispatcherMeta; +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher.ValidationResponse.Level; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; + +/** + * This dispatcher does not actually perform any crypto operations, but just forwards the string to be decrypted + * to a {@link MasterSource}. The given string is supposed to contain a valid source reference which is resolvable + * by one of the bound {@link MasterSource} implementations (and not actually an encrypted value). + * This dispatcher doesn't support encryption, but just validates and returns the given master source reference. + */ +@Singleton +@Named(MasterSourceLookupDispatcher.NAME) +public class MasterSourceLookupDispatcher implements Dispatcher, DispatcherMeta { + public static final String NAME = "masterSourceLookup"; + + protected final Collection sources; + + @Inject + public MasterSourceLookupDispatcher(Collection sources) { + this.sources = sources; + } + + @Override + public String name() { + return NAME; + } + + @Override + public String displayName() { + return "Master Source Lookup Dispatcher"; + } + + @Override + public Collection fields() { + return Collections.emptyList(); + } + + @Override + public EncryptPayload encrypt(String str, Map attributes, Map config) + throws SecDispatcherException { + // just make sure the given string is a valid reference! + decrypt(str, attributes, config); + return new EncryptPayload(attributes, str); + } + + @Override + public String decrypt(String str, Map attributes, Map config) + throws SecDispatcherException { + Optional plain = sources.stream() + .map(source -> source.handle(str)) + .filter(Objects::nonNull) + .findFirst(); + if (plain.isPresent()) { + return plain.get(); + } else { + throw new SecDispatcherException("No master source found for : " + str); + } + } + + @Override + public SecDispatcher.ValidationResponse validateConfiguration(Map config) { + // there is nothing really to validate without having a master reference at hand (which is outside the config) + Map> report = Collections.singletonMap( + SecDispatcher.ValidationResponse.Level.INFO, List.of("Configured Source configuration valid")); + return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), true, report, Collections.emptyList()); + } +} diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java index 352e01e..5b98639 100644 --- a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/DefaultSecDispatcherTest.java @@ -18,12 +18,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; import java.util.Map; import org.codehaus.plexus.components.secdispatcher.SecDispatcher; import org.codehaus.plexus.components.secdispatcher.internal.cipher.AESGCMNoPadding; import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterSourceLookupDispatcher; import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource; import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource; import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource; @@ -80,6 +83,18 @@ void masterWithSystemPropertyRoundTrip() throws Exception { roundtrip(); } + @Test + void masterSourceLookupWithEnvDecrypt() throws Exception { + saveSec("masterSourceLookup", Collections.emptyMap()); + assertDecrypted("{[name=masterSourceLookup,version=something]env:MASTER_PASSWORD}", "masterPw"); + } + + @Test + void masterSourceLookupWithSystemPropertyDecrypt() throws Exception { + saveSec("masterSourceLookup", Collections.emptyMap()); + assertDecrypted("{[name=masterSourceLookup,version=something]system-property:masterPassword}", "masterPw"); + } + @Test void validate() throws Exception { saveSec("master", Map.of("source", "system-property:masterPassword", "cipher", AESGCMNoPadding.CIPHER_ALG)); @@ -157,7 +172,7 @@ void detection() { protected void roundtrip() throws Exception { DefaultSecDispatcher sd = construct(); - assertEquals(2, sd.availableDispatchers().size()); + assertEquals(3, sd.availableDispatchers().size()); String encrypted = sd.encrypt("supersecret", Map.of(SecDispatcher.DISPATCHER_NAME_ATTR, "master", "a", "b")); // example: // {[name=master,cipher=AES/GCM/NoPadding,a=b]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==} @@ -170,6 +185,14 @@ protected void roundtrip() throws Exception { assertEquals("supersecret", pass); } + protected void assertDecrypted(String encrypted, String expectedPlainText) throws Exception { + DefaultSecDispatcher sd = construct(); + + assertEquals(3, sd.availableDispatchers().size()); + String plainText = sd.decrypt(encrypted); + assertEquals(expectedPlainText, plainText); + } + protected DefaultSecDispatcher construct() { return new DefaultSecDispatcher( Map.of( @@ -184,7 +207,10 @@ protected DefaultSecDispatcher construct() { GpgAgentMasterSource.NAME, new GpgAgentMasterSource())), "legacy", - new LegacyDispatcher()), + new LegacyDispatcher(), + "masterSourceLookup", + new MasterSourceLookupDispatcher(List.of( + new EnvMasterSource(), new SystemPropertyMasterSource(), new GpgAgentMasterSource()))), CONFIG_PATH); } diff --git a/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcherTest.java b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcherTest.java new file mode 100644 index 0000000..7c4fae6 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/dispatchers/MasterSourceLookupDispatcherTest.java @@ -0,0 +1,69 @@ +/* + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +package org.codehaus.plexus.components.secdispatcher.internal.dispatchers; + +import java.util.Collections; +import java.util.Map; + +import org.codehaus.plexus.components.secdispatcher.Dispatcher.EncryptPayload; +import org.codehaus.plexus.components.secdispatcher.SecDispatcher.ValidationResponse; +import org.codehaus.plexus.components.secdispatcher.SecDispatcherException; +import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MasterSourceLookupDispatcherTest { + + @Test + void testUnknownPrefix() { + MasterSourceLookupDispatcher masterSourceLookupDispatcher = + new MasterSourceLookupDispatcher(Collections.singleton(new EnvMasterSource())); + assertThrows( + SecDispatcherException.class, + () -> masterSourceLookupDispatcher.decrypt("unknown-prefix:test", Map.of(), Map.of())); + assertThrows( + SecDispatcherException.class, + () -> masterSourceLookupDispatcher.encrypt("unknown-prefix:test", Map.of(), Map.of())); + } + + @Test + void testSystemPropertyMasterSourceDecrypt() { + System.setProperty("myprop", "plaintext"); + MasterSourceLookupDispatcher masterSourceLookupDispatcher = + new MasterSourceLookupDispatcher(Collections.singleton(new SystemPropertyMasterSource())); + // SecDispatcher "un decorates" the PW + String cleartext = masterSourceLookupDispatcher.decrypt("system-property:myprop", Map.of(), Map.of()); + assertEquals("plaintext", cleartext); + } + + @Test + void testEncrypt() { + System.setProperty("myprop", "plaintext"); + MasterSourceLookupDispatcher masterSourceLookupDispatcher = + new MasterSourceLookupDispatcher(Collections.singleton(new SystemPropertyMasterSource())); + // SecDispatcher "un decorates" the PW + EncryptPayload payload = masterSourceLookupDispatcher.encrypt("system-property:myprop", Map.of(), Map.of()); + assertEquals("system-property:myprop", payload.getEncrypted()); + } + + @Test + void testValidateConfiguration() { + MasterSourceLookupDispatcher masterSourceLookupDispatcher = + new MasterSourceLookupDispatcher(Collections.singleton(new SystemPropertyMasterSource())); + ValidationResponse response = masterSourceLookupDispatcher.validateConfiguration(Collections.emptyMap()); + assertTrue(response.isValid()); + } +}