diff --git a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangDaemonConfig.java b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangDaemonConfig.java index 4d3a408f..c7f77c19 100644 --- a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangDaemonConfig.java +++ b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangDaemonConfig.java @@ -45,11 +45,11 @@ public class MultiLangDaemonConfig { private static final String PROP_PROCESSING_LANGUAGE = "processingLanguage"; private static final String PROP_MAX_ACTIVE_THREADS = "maxActiveThreads"; - private MultiLangDaemonConfiguration multiLangDaemonConfiguration; + private final MultiLangDaemonConfiguration multiLangDaemonConfiguration; - private ExecutorService executorService; + private final ExecutorService executorService; - private MultiLangRecordProcessorFactory recordProcessorFactory; + private final MultiLangRecordProcessorFactory recordProcessorFactory; /** * Constructor. @@ -165,7 +165,6 @@ public class MultiLangDaemonConfig { propertyStream.close(); } } - } private static boolean validateProperties(Properties properties) { @@ -182,12 +181,12 @@ public class MultiLangDaemonConfig { log.debug("Value for {} property is {}", PROP_MAX_ACTIVE_THREADS, maxActiveThreads); if (maxActiveThreads <= 0) { log.info("Using a cached thread pool."); - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue(), + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), builder.build()); } else { log.info("Using a fixed thread pool with {} max active threads.", maxActiveThreads); return new ThreadPoolExecutor(maxActiveThreads, maxActiveThreads, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), builder.build()); + new LinkedBlockingQueue<>(), builder.build()); } } diff --git a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangShardRecordProcessor.java b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangShardRecordProcessor.java index 7b0eefe2..241ea8ee 100644 --- a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangShardRecordProcessor.java +++ b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/MultiLangShardRecordProcessor.java @@ -32,7 +32,6 @@ import software.amazon.kinesis.lifecycle.events.ShutdownRequestedInput; import software.amazon.kinesis.multilang.config.MultiLangDaemonConfiguration; import software.amazon.kinesis.processor.ShardRecordProcessor; - /** * A record processor that manages creating a child process that implements the multi language protocol and connecting * that child process's input and outputs to a {@link MultiLangProtocol} object and calling the appropriate methods on @@ -50,20 +49,20 @@ public class MultiLangShardRecordProcessor implements ShardRecordProcessor { private Future stderrReadTask; - private MessageWriter messageWriter; - private MessageReader messageReader; - private DrainChildSTDERRTask readSTDERRTask; + private final MessageWriter messageWriter; + private final MessageReader messageReader; + private final DrainChildSTDERRTask readSTDERRTask; - private ProcessBuilder processBuilder; + private final ProcessBuilder processBuilder; private Process process; - private ExecutorService executorService; + private final ExecutorService executorService; private ProcessState state; - private ObjectMapper objectMapper; + private final ObjectMapper objectMapper; private MultiLangProtocol protocol; - private MultiLangDaemonConfiguration configuration; + private final MultiLangDaemonConfiguration configuration; @Override public void initialize(InitializationInput initializationInput) { @@ -213,7 +212,6 @@ public class MultiLangShardRecordProcessor implements ShardRecordProcessor { this.readSTDERRTask = readSTDERRTask; this.configuration = configuration; - this.state = ProcessState.ACTIVE; } @@ -303,8 +301,6 @@ public class MultiLangShardRecordProcessor implements ShardRecordProcessor { /** * We provide a package level method for unit testing this call to exit. - * - * @param val exit value */ void exit() { System.exit(EXIT_VALUE); diff --git a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/NestedPropertyKey.java b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/NestedPropertyKey.java new file mode 100644 index 00000000..cca83062 --- /dev/null +++ b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/NestedPropertyKey.java @@ -0,0 +1,126 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.kinesis.multilang; + +import java.util.HashMap; +import java.util.Map; + +import com.google.common.base.CaseFormat; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Key-Value pairs which may be nested in, and extracted from, a property value + * in a Java properties file. For example, given the line in a property file of + * {@code my_key = my_value|foo=bar} and a delimiter split on {@code |} (pipe), + * the value {@code my_value|foo=bar} would have a nested key of {@code foo} + * and its corresponding value is {@code bar}. + *

+ * The order of nested properties does not matter, and these properties are optional. + * Customers may choose to provide, in any order, zero-or-more nested properties. + *

+ * Duplicate keys are not supported, and may result in a last-write-wins outcome. + */ +@Slf4j +public enum NestedPropertyKey { + + /** + * Specify the service endpoint where requests will be submitted. + * This property's value must be in the following format: + *
+     *     ENDPOINT ::= SERVICE_ENDPOINT "^" SIGNING_REGION
+     *     SERVICE_ENDPOINT ::= URL
+     *     SIGNING_REGION ::= AWS_REGION
+     * 
+ * + * @see AWS Service endpoints + * @see Available Regions + */ + ENDPOINT { + void visit(final NestedPropertyProcessor processor, final String endpoint) { + final String[] tokens = endpoint.split("\\^"); + if (tokens.length != 2) { + throw new IllegalArgumentException("Invalid " + name() + ": " + endpoint); + } + processor.acceptEndpoint(tokens[0], tokens[1]); + } + }, + + /** + * External ids may be used when delegating access in a multi-tenant + * environment, or to third parties. + * + * @see + * How to use an external ID when granting access to your AWS resources to a third party + */ + EXTERNAL_ID { + void visit(final NestedPropertyProcessor processor, final String externalId) { + processor.acceptExternalId(externalId); + } + }, + + ; + + /** + * Nested key within the property value. For example, a nested key-value + * of {@code foo=bar} has a nested key of {@code foo}. + */ + @Getter(AccessLevel.PACKAGE) + private final String nestedKey; + + NestedPropertyKey() { + // convert the enum from UPPER_SNAKE_CASE to lowerCamelCase + nestedKey = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name()); + } + + abstract void visit(NestedPropertyProcessor processor, String value); + + /** + * Parses any number of parameters. Each nested property will prompt a + * visit to the {@code processor}. + * + * @param processor processor to be invoked for every nested property + * @param params parameters to check for a nested property key + */ + public static void parse(final NestedPropertyProcessor processor, final String... params) { + // Construct a disposable cache to keep this O(n). Since parsing is + // usually one-and-done, it's wasteful to maintain this cache in perpetuity. + final Map cachedKeys = new HashMap<>(); + for (final NestedPropertyKey npk : values()) { + cachedKeys.put(npk.getNestedKey(), npk); + } + + for (final String param : params) { + if (param != null) { + final String[] tokens = param.split("="); + if (tokens.length == 2) { + final NestedPropertyKey npk = cachedKeys.get(tokens[0]); + if (npk != null) { + npk.visit(processor, tokens[1]); + } else { + log.warn("Unsupported nested key: {}", param); + } + } else if (tokens.length > 2) { + log.warn("Malformed nested key: {}", param); + } else { + log.info("Parameter is not a nested key: {}", param); + } + } + } + } + +} diff --git a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/NestedPropertyProcessor.java b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/NestedPropertyProcessor.java new file mode 100644 index 00000000..ee623174 --- /dev/null +++ b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/NestedPropertyProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.kinesis.multilang; + +/** + * Defines methods to process {@link NestedPropertyKey}s. + */ +public interface NestedPropertyProcessor { + + /** + * Set the external id, an optional field to designate who can assume an IAM role. + * + * @param externalId external id used in the service call used to retrieve session credentials + */ + void acceptExternalId(String externalId); + + /** + * Set the service endpoint where requests are sent. + * + * @param serviceEndpoint the service endpoint either with or without the protocol + * (e.g. https://sns.us-west-1.amazonaws.com or sns.us-west-1.amazonaws.com) + * @param signingRegion the region to use for SigV4 signing of requests (e.g. us-west-1) + * + * @see + * AwsClientBuilder.EndpointConfiguration + */ + void acceptEndpoint(String serviceEndpoint, String signingRegion); + +} diff --git a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/auth/KclSTSAssumeRoleSessionCredentialsProvider.java b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/auth/KclSTSAssumeRoleSessionCredentialsProvider.java new file mode 100644 index 00000000..c95855c0 --- /dev/null +++ b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/auth/KclSTSAssumeRoleSessionCredentialsProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.kinesis.multilang.auth; + +import java.util.Arrays; + +import com.amazonaws.auth.AWSSessionCredentials; +import com.amazonaws.auth.AWSSessionCredentialsProvider; +import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; +import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider.Builder; +import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.securitytoken.AWSSecurityTokenService; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient; + +import software.amazon.kinesis.multilang.NestedPropertyKey; +import software.amazon.kinesis.multilang.NestedPropertyProcessor; + +/** + * An {@link AWSSessionCredentialsProvider} that is backed by STSAssumeRole. + */ +public class KclSTSAssumeRoleSessionCredentialsProvider + implements AWSSessionCredentialsProvider, NestedPropertyProcessor { + + private final Builder builder; + + private final STSAssumeRoleSessionCredentialsProvider provider; + + /** + * + * @param params vararg parameters which must include roleArn at index=0, + * and roleSessionName at index=1 + */ + public KclSTSAssumeRoleSessionCredentialsProvider(final String[] params) { + this(params[0], params[1], Arrays.copyOfRange(params, 2, params.length)); + } + + public KclSTSAssumeRoleSessionCredentialsProvider(final String roleArn, final String roleSessionName, + final String... params) { + builder = new Builder(roleArn, roleSessionName); + NestedPropertyKey.parse(this, params); + provider = builder.build(); + } + + @Override + public AWSSessionCredentials getCredentials() { + return provider.getCredentials(); + } + + @Override + public void refresh() { + // do nothing + } + + @Override + public void acceptExternalId(final String externalId) { + builder.withExternalId(externalId); + } + + @Override + public void acceptEndpoint(final String serviceEndpoint, final String signingRegion) { + final EndpointConfiguration endpoint = new EndpointConfiguration(serviceEndpoint, signingRegion); + final AWSSecurityTokenService stsClient = + AWSSecurityTokenServiceClient.builder() + .withEndpointConfiguration(endpoint) + .withRegion(Regions.fromName(signingRegion)) + .build(); + builder.withStsClient(stsClient); + } + +} \ No newline at end of file diff --git a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoder.java b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoder.java index 97fa975e..5f124bfc 100644 --- a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoder.java +++ b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoder.java @@ -17,7 +17,10 @@ package software.amazon.kinesis.multilang.config; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSCredentialsProviderChain; @@ -28,7 +31,6 @@ import lombok.extern.slf4j.Slf4j; */ @Slf4j class AWSCredentialsProviderPropertyValueDecoder implements IPropertyValueDecoder { - private static final String AUTH_PREFIX = "com.amazonaws.auth."; private static final String LIST_DELIMITER = ","; private static final String ARG_DELIMITER = "|"; @@ -63,35 +65,65 @@ class AWSCredentialsProviderPropertyValueDecoder implements IPropertyValueDecode */ @Override public List> getSupportedTypes() { - return Arrays.asList(AWSCredentialsProvider.class); + return Collections.singletonList(AWSCredentialsProvider.class); } - /* + /** * Convert string list to a list of valid credentials providers. */ private static List getValidCredentialsProviders(List providerNames) { List credentialsProviders = new ArrayList<>(); + for (String providerName : providerNames) { - if (providerName.contains(ARG_DELIMITER)) { - String[] nameAndArgs = providerName.split("\\" + ARG_DELIMITER); - Class[] argTypes = new Class[nameAndArgs.length - 1]; - Arrays.fill(argTypes, String.class); + final String[] nameAndArgs = providerName.split("\\" + ARG_DELIMITER); + final Class clazz; + try { + final Class c = Class.forName(nameAndArgs[0]); + if (!AWSCredentialsProvider.class.isAssignableFrom(c)) { + continue; + } + clazz = (Class) c; + } catch (ClassNotFoundException cnfe) { + continue; + } + + AWSCredentialsProvider provider = null; + if (nameAndArgs.length > 1) { + final String[] varargs = Arrays.copyOfRange(nameAndArgs, 1, nameAndArgs.length); + + // attempt to invoke an explicit N-arg constructor of FooClass(String, String, ...) try { - Class className = Class.forName(nameAndArgs[0]); - Constructor c = className.getConstructor(argTypes); - credentialsProviders.add((AWSCredentialsProvider) c - .newInstance(Arrays.copyOfRange(nameAndArgs, 1, nameAndArgs.length))); + Class[] argTypes = new Class[nameAndArgs.length - 1]; + Arrays.fill(argTypes, String.class); + Constructor c = clazz.getConstructor(argTypes); + provider = c.newInstance(varargs); } catch (Exception e) { log.debug("Can't find any credentials provider matching {}.", providerName); } - } else { + + if (provider == null) { + // attempt to invoke a public varargs/array constructor of FooClass(String[]) + try { + Constructor c = clazz.getConstructor(String[].class); + provider = c.newInstance((Object) varargs); + } catch (Exception e) { + log.debug("Can't find any credentials provider matching {}.", providerName); + } + } + } + + if (provider == null) { + // regardless of parameters, fallback to invoke a public no-arg constructor try { - Class className = Class.forName(providerName); - credentialsProviders.add((AWSCredentialsProvider) className.newInstance()); + provider = clazz.newInstance(); } catch (Exception e) { log.debug("Can't find any credentials provider matching {}.", providerName); } } + + if (provider != null) { + credentialsProviders.add(provider); + } } return credentialsProviders; } @@ -99,7 +131,7 @@ class AWSCredentialsProviderPropertyValueDecoder implements IPropertyValueDecode private static List getProviderNames(String property) { // assume list delimiter is "," String[] elements = property.split(LIST_DELIMITER); - List result = new ArrayList(); + List result = new ArrayList<>(); for (int i = 0; i < elements.length; i++) { String string = elements[i].trim(); if (!string.isEmpty()) { @@ -110,20 +142,20 @@ class AWSCredentialsProviderPropertyValueDecoder implements IPropertyValueDecode return result; } - private static List getPossibleFullClassNames(String s) { - /* - * We take care of three cases : - * - * 1. Customer provides a short name of common providers in com.amazonaws.auth package i.e. any classes - * implementing the AWSCredentialsProvider interface: - * http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/AWSCredentialsProvider.html - * - * 2. Customer provides a full name of common providers e.g. com.amazonaws.auth.ClasspathFileCredentialsProvider - * - * 3. Customer provides a custom credentials provider with full name of provider - */ + private static List getPossibleFullClassNames(final String provider) { + return Stream.of( + // Customer provides a short name of common providers in com.amazonaws.auth package + // (e.g., any classes implementing the AWSCredentialsProvider interface) + // @see http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/AWSCredentialsProvider.html + "com.amazonaws.auth.", - return Arrays.asList(s, AUTH_PREFIX + s); + // Customer provides a short name of a provider offered by this multi-lang package + "software.amazon.kinesis.multilang.auth.", + + // Customer provides a fully-qualified provider name, or a custom credentials provider + // (e.g., com.amazonaws.auth.ClasspathFileCredentialsProvider, org.mycompany.FooProvider) + "" + ).map(prefix -> prefix + provider).collect(Collectors.toList()); } } diff --git a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/BuilderDynaBean.java b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/BuilderDynaBean.java index 2035695c..5baa47f4 100644 --- a/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/BuilderDynaBean.java +++ b/amazon-kinesis-client-multilang/src/main/java/software/amazon/kinesis/multilang/config/BuilderDynaBean.java @@ -57,10 +57,10 @@ public class BuilderDynaBean implements DynaBean { } public BuilderDynaBean(Class destinedClass, ConvertUtilsBean convertUtilsBean, - Function emtpyPropertyHandler, List classPrefixSearchList) { + Function emptyPropertyHandler, List classPrefixSearchList) { this.convertUtilsBean = convertUtilsBean; this.classPrefixSearchList = classPrefixSearchList; - this.emptyPropertyHandler = emtpyPropertyHandler; + this.emptyPropertyHandler = emptyPropertyHandler; initialize(destinedClass); } diff --git a/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/MultiLangDaemonConfigTest.java b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/MultiLangDaemonConfigTest.java index c5740a2f..ae7c9939 100644 --- a/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/MultiLangDaemonConfigTest.java +++ b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/MultiLangDaemonConfigTest.java @@ -62,7 +62,6 @@ public class MultiLangDaemonConfigTest { * @throws IOException */ public void setup(String streamName, String streamArn) throws IOException { - String properties = String.format("executableName = %s\n" + "applicationName = %s\n" + "AWSCredentialsProvider = DefaultAWSCredentialsProviderChain\n" diff --git a/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/NestedPropertyKeyTest.java b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/NestedPropertyKeyTest.java new file mode 100644 index 00000000..43bdb53e --- /dev/null +++ b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/NestedPropertyKeyTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.kinesis.multilang; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static software.amazon.kinesis.multilang.NestedPropertyKey.ENDPOINT; +import static software.amazon.kinesis.multilang.NestedPropertyKey.EXTERNAL_ID; +import static software.amazon.kinesis.multilang.NestedPropertyKey.parse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NestedPropertyKeyTest { + + @Mock + private NestedPropertyProcessor mockProcessor; + + @Test + public void testExternalId() { + final String expectedId = "eid"; + + parse(mockProcessor, createKey(EXTERNAL_ID, expectedId)); + verify(mockProcessor).acceptExternalId(expectedId); + } + + @Test + public void testEndpoint() { + final String expectedEndpoint = "https://sts.us-east-1.amazon.com"; + final String expectedRegion = "us-east-1"; + final String param = createKey(ENDPOINT, expectedEndpoint + "^" + expectedRegion); + + parse(mockProcessor, param); + verify(mockProcessor).acceptEndpoint(expectedEndpoint, expectedRegion); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidEndpoint() { + parse(mockProcessor, createKey(ENDPOINT, "value-sans-caret-delimiter")); + } + + /** + * Test that the literal nested key (i.e., {@code key=} in {@code some_val|key=nested_val}) + * does not change. Any change to an existing literal key is not backwards-compatible. + */ + @Test + public void testKeysExplicitly() { + // Adding a new enum will deliberately cause this assert to fail, and + // therefore raise awareness for this explicit test. Add-and-remove may + // keep the number unchanged yet will also break (by removing an enum). + assertEquals(2, NestedPropertyKey.values().length); + + assertEquals("endpoint", ENDPOINT.getNestedKey()); + assertEquals("externalId", EXTERNAL_ID.getNestedKey()); + } + + @Test + public void testNonmatchingParameters() { + final String[] params = new String[] { + null, + "", + "hello world", // no nested key + "foo=bar", // nested key, but is not a recognized key + createKey(EXTERNAL_ID, "eid") + "=extra", // valid key made invalid by second '=' + }; + parse(mockProcessor, params); + verifyZeroInteractions(mockProcessor); + } + + private static String createKey(final NestedPropertyKey key, final String value) { + return key.getNestedKey() + "=" + value; + } + +} \ No newline at end of file diff --git a/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/auth/KclSTSAssumeRoleSessionCredentialsProviderTest.java b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/auth/KclSTSAssumeRoleSessionCredentialsProviderTest.java new file mode 100644 index 00000000..20e4bc8a --- /dev/null +++ b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/auth/KclSTSAssumeRoleSessionCredentialsProviderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.kinesis.multilang.auth; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class KclSTSAssumeRoleSessionCredentialsProviderTest { + + @Test + public void testVarArgs() { + for (final String[] varargs : Arrays.asList( + new String[] { "arn", "session", "externalId=eid", "foo"}, + new String[] { "arn", "session", "foo", "externalId=eid"} + )) { + final VarArgsSpy provider = new VarArgsSpy(varargs); + assertEquals("eid", provider.externalId); + } + } + + private static class VarArgsSpy extends KclSTSAssumeRoleSessionCredentialsProvider { + + private String externalId; + + public VarArgsSpy(String[] args) { + super(args); + } + + @Override + public void acceptExternalId(final String externalId) { + this.externalId = externalId; + super.acceptExternalId(externalId); + } + } +} \ No newline at end of file diff --git a/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoderTest.java b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoderTest.java index ced63f24..80e67d26 100644 --- a/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoderTest.java +++ b/amazon-kinesis-client-multilang/src/test/java/software/amazon/kinesis/multilang/config/AWSCredentialsProviderPropertyValueDecoderTest.java @@ -16,6 +16,8 @@ package software.amazon.kinesis.multilang.config; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import java.util.Arrays; @@ -26,6 +28,7 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.junit.Test; +import software.amazon.kinesis.multilang.auth.KclSTSAssumeRoleSessionCredentialsProvider; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; @@ -38,7 +41,7 @@ public class AWSCredentialsProviderPropertyValueDecoderTest { private final String credentialName1 = AlwaysSucceedCredentialsProvider.class.getName(); private final String credentialName2 = ConstructorCredentialsProvider.class.getName(); - private AWSCredentialsProviderPropertyValueDecoder decoder = new AWSCredentialsProviderPropertyValueDecoder(); + private final AWSCredentialsProviderPropertyValueDecoder decoder = new AWSCredentialsProviderPropertyValueDecoder(); @ToString private static class AWSCredentialsMatcher extends TypeSafeDiagnosingMatcher { @@ -53,10 +56,6 @@ public class AWSCredentialsProviderPropertyValueDecoderTest { this.classMatcher = instanceOf(AWSCredentialsProviderChain.class); } - private AWSCredentialsMatcher(AWSCredentials expected) { - this(expected.getAWSAccessKeyId(), expected.getAWSSecretKey()); - } - @Override protected boolean matchesSafely(AWSCredentialsProvider item, Description mismatchDescription) { AWSCredentials actual = item.getCredentials(); @@ -114,6 +113,33 @@ public class AWSCredentialsProviderPropertyValueDecoderTest { assertThat(provider, hasCredentials("arg1", "arg2")); } + /** + * Test that providers in the multi-lang auth package can be resolved and instantiated. + */ + @Test + public void testKclAuthProvider() { + for (final String className : Arrays.asList( + KclSTSAssumeRoleSessionCredentialsProvider.class.getName(), // fully-qualified name + KclSTSAssumeRoleSessionCredentialsProvider.class.getSimpleName() // name-only; needs prefix + )) { + final AWSCredentialsProvider provider = decoder.decodeValue(className + "|arn|sessionName"); + assertNotNull(className, provider); + } + } + + /** + * Test that a provider can be instantiated by its varargs constructor. + */ + @Test + public void testVarArgAuthProvider() { + final String[] args = new String[] { "arg1", "arg2", "arg3" }; + final String className = VarArgCredentialsProvider.class.getName(); + final String encodedValue = className + "|" + String.join("|", args); + + final AWSCredentialsProvider provider = decoder.decodeValue(encodedValue); + assertEquals(Arrays.toString(args), provider.getCredentials().getAWSAccessKeyId()); + } + /** * This credentials provider will always succeed */ @@ -138,9 +164,9 @@ public class AWSCredentialsProviderPropertyValueDecoderTest { private String arg1; private String arg2; + @SuppressWarnings("unused") public ConstructorCredentialsProvider(String arg1) { - this.arg1 = arg1; - this.arg2 = "blank"; + this(arg1, "blank"); } public ConstructorCredentialsProvider(String arg1, String arg2) { @@ -158,4 +184,25 @@ public class AWSCredentialsProviderPropertyValueDecoderTest { } } + + private static class VarArgCredentialsProvider implements AWSCredentialsProvider { + + private final String[] args; + + public VarArgCredentialsProvider(final String[] args) { + this.args = args; + } + + @Override + public AWSCredentials getCredentials() { + // KISS solution to surface the constructor args + final String flattenedArgs = Arrays.toString(args); + return new BasicAWSCredentials(flattenedArgs, flattenedArgs); + } + + @Override + public void refresh() { + + } + } }