This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 315e54a812 NIFI-9636: Adding AwsSecretsManagerParameterProvider
315e54a812 is described below
commit 315e54a812947494f1fb32fb49997505f0339d3c
Author: Joe Gresock <[email protected]>
AuthorDate: Sat Sep 10 10:01:58 2022 -0400
NIFI-9636: Adding AwsSecretsManagerParameterProvider
This closes #6392
Signed-off-by: David Handermann <[email protected]>
---
.../nifi-aws-bundle/nifi-aws-nar/pom.xml | 5 +
.../pom.xml | 43 ++-
.../aws/AwsSecretsManagerParameterProvider.java | 297 +++++++++++++++++++++
.../org.apache.nifi.parameter.ParameterProvider | 16 ++
.../additionalDetails.html | 62 +++++
.../TestAwsSecretsManagerParameterProvider.java | 199 ++++++++++++++
nifi-nar-bundles/nifi-aws-bundle/pom.xml | 1 +
7 files changed, 612 insertions(+), 11 deletions(-)
diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-nar/pom.xml
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-nar/pom.xml
index ea980b7490..984f1b179b 100644
--- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-nar/pom.xml
+++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-nar/pom.xml
@@ -41,6 +41,11 @@
<artifactId>nifi-aws-processors</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-aws-parameter-providers</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-aws-parameter-value-providers</artifactId>
diff --git a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-nar/pom.xml
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/pom.xml
similarity index 59%
copy from nifi-nar-bundles/nifi-aws-bundle/nifi-aws-nar/pom.xml
copy to nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/pom.xml
index ea980b7490..282f89f576 100644
--- a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-nar/pom.xml
+++ b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/pom.xml
@@ -15,40 +15,61 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
-
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-aws-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
- <artifactId>nifi-aws-nar</artifactId>
- <packaging>nar</packaging>
- <properties>
- <maven.javadoc.skip>true</maven.javadoc.skip>
- <source.skip>true</source.skip>
- </properties>
+ <artifactId>nifi-aws-parameter-providers</artifactId>
+ <packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
- <artifactId>nifi-aws-service-api-nar</artifactId>
+ <artifactId>nifi-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-aws-service-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
- <type>nar</type>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
- <artifactId>nifi-aws-processors</artifactId>
+ <artifactId>nifi-ssl-context-service-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
- <artifactId>nifi-aws-parameter-value-providers</artifactId>
+ <artifactId>nifi-utils</artifactId>
<version>1.18.0-SNAPSHOT</version>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.amazonaws</groupId>
+ <artifactId>aws-java-sdk-secretsmanager</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-expression-language</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-mock</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/java/org/apache/nifi/parameter/aws/AwsSecretsManagerParameterProvider.java
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/java/org/apache/nifi/parameter/aws/AwsSecretsManagerParameterProvider.java
new file mode 100644
index 0000000000..4cab6fa9a2
--- /dev/null
+++
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/java/org/apache/nifi/parameter/aws/AwsSecretsManagerParameterProvider.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.parameter.aws;
+
+import com.amazonaws.ClientConfiguration;
+import com.amazonaws.Protocol;
+import com.amazonaws.auth.AWSCredentialsProvider;
+import com.amazonaws.http.conn.ssl.SdkTLSSocketFactory;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.secretsmanager.AWSSecretsManager;
+import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder;
+import com.amazonaws.services.secretsmanager.model.AWSSecretsManagerException;
+import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest;
+import com.amazonaws.services.secretsmanager.model.GetSecretValueResult;
+import com.amazonaws.services.secretsmanager.model.ListSecretsRequest;
+import com.amazonaws.services.secretsmanager.model.ListSecretsResult;
+import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException;
+import com.amazonaws.services.secretsmanager.model.SecretListEntry;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.parameter.AbstractParameterProvider;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.VerifiableParameterProvider;
+import org.apache.nifi.processor.util.StandardValidators;
+import
org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderService;
+import org.apache.nifi.ssl.SSLContextService;
+
+import javax.net.ssl.SSLContext;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * Reads secrets from AWS Secrets Manager to provide parameter values.
Secrets must be created similar to the following AWS cli command: <br/><br/>
+ * <code>aws secretsmanager create-secret --name "[Context]" --secret-string
'{ "[Param]": "[secretValue]", "[Param2]": "[secretValue2]" }'</code> <br/><br/>
+ *
+ */
+
+@Tags({"aws", "secretsmanager", "secrets", "manager"})
+@CapabilityDescription("Fetches parameters from AWS SecretsManager. Each
secret becomes a Parameter group, which can map to a Parameter Context, with " +
+ "key/value pairs in the secret mapping to Parameters in the group.")
+public class AwsSecretsManagerParameterProvider extends
AbstractParameterProvider implements VerifiableParameterProvider {
+
+ public static final PropertyDescriptor SECRET_NAME_PATTERN = new
PropertyDescriptor.Builder()
+ .name("secret-name-pattern")
+ .displayName("Secret Name Pattern")
+ .description("A Regular Expression matching on Secret Name that
identifies Secrets whose parameters should be fetched. " +
+ "Any secrets whose names do not match this pattern will
not be fetched.")
+ .addValidator(StandardValidators.REGULAR_EXPRESSION_VALIDATOR)
+ .required(true)
+ .defaultValue(".*")
+ .build();
+ /**
+ * AWS credentials provider service
+ *
+ * @see <a
href="http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/AWSCredentialsProvider.html">AWSCredentialsProvider</a>
+ */
+ public static final PropertyDescriptor AWS_CREDENTIALS_PROVIDER_SERVICE =
new PropertyDescriptor.Builder()
+ .name("aws-credentials-provider-service")
+ .displayName("AWS Credentials Provider Service")
+ .description("Service used to obtain an Amazon Web Services
Credentials Provider")
+ .required(true)
+ .identifiesControllerService(AWSCredentialsProviderService.class)
+ .build();
+
+ public static final PropertyDescriptor REGION = new
PropertyDescriptor.Builder()
+ .name("aws-region")
+ .displayName("Region")
+ .required(true)
+ .allowableValues(getAvailableRegions())
+
.defaultValue(createAllowableValue(Regions.DEFAULT_REGION).getValue())
+ .build();
+
+ public static final PropertyDescriptor TIMEOUT = new
PropertyDescriptor.Builder()
+ .name("aws-communications-timeout")
+ .displayName("Communications Timeout")
+ .required(true)
+ .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+ .defaultValue("30 secs")
+ .build();
+
+ public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new
PropertyDescriptor.Builder()
+ .name("aws-ssl-context-service")
+ .displayName("SSL Context Service")
+ .description("Specifies an optional SSL Context Service that, if
provided, will be used to create connections")
+ .required(false)
+ .identifiesControllerService(SSLContextService.class)
+ .build();
+
+ private static final String DEFAULT_USER_AGENT = "NiFi";
+ private static final Protocol DEFAULT_PROTOCOL = Protocol.HTTPS;
+ private static final List<PropertyDescriptor> PROPERTIES =
Collections.unmodifiableList(Arrays.asList(
+ SECRET_NAME_PATTERN,
+ REGION,
+ AWS_CREDENTIALS_PROVIDER_SERVICE,
+ TIMEOUT,
+ SSL_CONTEXT_SERVICE
+ ));
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return PROPERTIES;
+ }
+
+ @Override
+ public List<ParameterGroup> fetchParameters(final ConfigurationContext
context) {
+ AWSSecretsManager secretsManager = this.configureClient(context);
+
+ final List<ParameterGroup> groups = new ArrayList<>();
+ final ListSecretsRequest listSecretsRequest = new ListSecretsRequest();
+ final ListSecretsResult listSecretsResult =
secretsManager.listSecrets(listSecretsRequest);
+ for (final SecretListEntry entry : listSecretsResult.getSecretList()) {
+ groups.addAll(fetchSecret(secretsManager, context,
entry.getName()));
+ }
+
+ return groups;
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final ConfigurationContext
context, final ComponentLog verificationLogger) {
+ final List<ConfigVerificationResult> results = new ArrayList<>();
+
+ try {
+ final List<ParameterGroup> parameterGroups =
fetchParameters(context);
+ int parameterCount = 0;
+ for (final ParameterGroup group : parameterGroups) {
+ parameterCount += group.getParameters().size();
+ }
+ results.add(new ConfigVerificationResult.Builder()
+ .outcome(ConfigVerificationResult.Outcome.SUCCESSFUL)
+ .verificationStepName("Fetch Parameters")
+ .explanation(String.format("Fetched secret keys [%d] as
parameters, across groups [%d]",
+ parameterCount, parameterGroups.size()))
+ .build());
+ } catch (final Exception e) {
+ verificationLogger.error("Failed to fetch parameters", e);
+ results.add(new ConfigVerificationResult.Builder()
+ .outcome(ConfigVerificationResult.Outcome.FAILED)
+ .verificationStepName("Fetch Parameters")
+ .explanation("Failed to fetch parameters: " +
e.getMessage())
+ .build());
+ }
+ return results;
+ }
+
+ private List<ParameterGroup> fetchSecret(final AWSSecretsManager
secretsManager, final ConfigurationContext context, final String secretName) {
+ final List<ParameterGroup> groups = new ArrayList<>();
+ final Pattern secretNamePattern =
Pattern.compile(context.getProperty(SECRET_NAME_PATTERN).getValue());
+
+ final List<Parameter> parameters = new ArrayList<>();
+
+ if (!secretNamePattern.matcher(secretName).matches()) {
+ getLogger().debug("Secret [{}] does not match the secret name
pattern {}", secretName, secretNamePattern);
+ return groups;
+ }
+
+ final GetSecretValueRequest getSecretValueRequest = new
GetSecretValueRequest().withSecretId(secretName);
+ try {
+ final GetSecretValueResult getSecretValueResult =
secretsManager.getSecretValue(getSecretValueRequest);
+
+ if (getSecretValueResult.getSecretString() == null) {
+ getLogger().debug("Secret [{}] is not configured", secretName);
+ return groups;
+ }
+
+ final ObjectNode secretObject =
parseSecret(getSecretValueResult.getSecretString());
+ if (secretObject == null) {
+ getLogger().debug("Secret [{}] is not in the expected JSON
key/value format", secretName);
+ return groups;
+ }
+
+ for (final Iterator<Map.Entry<String, JsonNode>> it =
secretObject.fields(); it.hasNext(); ) {
+ final Map.Entry<String, JsonNode> field = it.next();
+ final String parameterName = field.getKey();
+ final String parameterValue = field.getValue().textValue();
+ if (parameterValue == null) {
+ getLogger().debug("Secret [{}] Parameter [{}] has no
value", secretName, parameterName);
+ continue;
+ }
+
+ parameters.add(createParameter(parameterName, parameterValue));
+ }
+
+ groups.add(new ParameterGroup(secretName, parameters));
+
+ return groups;
+ } catch (final ResourceNotFoundException e) {
+ throw new IllegalStateException(String.format("Secret %s not
found", secretName), e);
+ } catch (final AWSSecretsManagerException e) {
+ throw new IllegalStateException("Error retrieving secret " +
secretName, e);
+ }
+ }
+
+ private Parameter createParameter(final String parameterName, final String
parameterValue) {
+ final ParameterDescriptor parameterDescriptor = new
ParameterDescriptor.Builder().name(parameterName).build();
+ return new Parameter(parameterDescriptor, parameterValue, null, true);
+ }
+
+ protected ClientConfiguration createConfiguration(final
ConfigurationContext context) {
+ final ClientConfiguration config = new ClientConfiguration();
+ config.setMaxErrorRetry(0);
+ config.setUserAgentPrefix(DEFAULT_USER_AGENT);
+ config.setProtocol(DEFAULT_PROTOCOL);
+ final int commsTimeout =
context.getProperty(TIMEOUT).asTimePeriod(TimeUnit.MILLISECONDS).intValue();
+ config.setConnectionTimeout(commsTimeout);
+ config.setSocketTimeout(commsTimeout);
+
+ final SSLContextService sslContextService =
context.getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class);
+ if (sslContextService != null) {
+ final SSLContext sslContext = sslContextService.createContext();
+ SdkTLSSocketFactory sdkTLSSocketFactory = new
SdkTLSSocketFactory(sslContext,
SdkTLSSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
+
config.getApacheHttpClientConfig().setSslSocketFactory(sdkTLSSocketFactory);
+ }
+
+ return config;
+ }
+
+ private ObjectNode parseSecret(final String secretString) {
+ try {
+ final JsonNode root = objectMapper.readTree(secretString);
+ if (root instanceof ObjectNode) {
+ return (ObjectNode) root;
+ }
+ return null;
+ } catch (final JsonProcessingException e) {
+ getLogger().debug("Error parsing JSON", e);
+ return null;
+ }
+ }
+
+ AWSSecretsManager configureClient(final ConfigurationContext context) {
+ return AWSSecretsManagerClientBuilder.standard()
+ .withRegion(context.getProperty(REGION).getValue())
+ .withClientConfiguration(createConfiguration(context))
+ .withCredentials(getCredentialsProvider(context))
+ .build();
+ }
+
+ /**
+ * Get credentials provider using the {@link AWSCredentialsProviderService}
+ * @param context the configuration context
+ * @return AWSCredentialsProvider the credential provider
+ * @see <a
href="http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/AWSCredentialsProvider.html">AWSCredentialsProvider</a>
+ */
+ protected AWSCredentialsProvider getCredentialsProvider(final
ConfigurationContext context) {
+
+ final AWSCredentialsProviderService awsCredentialsProviderService =
+
context.getProperty(AWS_CREDENTIALS_PROVIDER_SERVICE).asControllerService(AWSCredentialsProviderService.class);
+
+ return awsCredentialsProviderService.getCredentialsProvider();
+
+ }
+
+ private static AllowableValue createAllowableValue(final Regions region) {
+ return new AllowableValue(region.getName(), region.getDescription(),
"AWS Region Code : " + region.getName());
+ }
+
+ private static AllowableValue[] getAvailableRegions() {
+ final List<AllowableValue> values = new ArrayList<>();
+ for (final Regions region : Regions.values()) {
+ values.add(createAllowableValue(region));
+ }
+ return values.toArray(new AllowableValue[0]);
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/resources/META-INF/services/org.apache.nifi.parameter.ParameterProvider
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/resources/META-INF/services/org.apache.nifi.parameter.ParameterProvider
new file mode 100644
index 0000000000..e4fad1ecbc
--- /dev/null
+++
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/resources/META-INF/services/org.apache.nifi.parameter.ParameterProvider
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+org.apache.nifi.parameter.aws.AwsSecretsManagerParameterProvider
+
diff --git
a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/resources/docs/org.apache.nifi.parameter.aws.AwsSecretsManagerParameterProvider/additionalDetails.html
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/resources/docs/org.apache.nifi.parameter.aws.AwsSecretsManagerParameterProvider/additionalDetails.html
new file mode 100644
index 0000000000..5974197a8f
--- /dev/null
+++
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/main/resources/docs/org.apache.nifi.parameter.aws.AwsSecretsManagerParameterProvider/additionalDetails.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html lang="en">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+<head>
+ <meta charset="utf-8" />
+ <title>AWSSecretsManagerParameterProvider</title>
+
+ <link rel="stylesheet" href="../../../../../css/component-usage.css"
type="text/css" />
+</head>
+<body>
+ <h3>Mapping AWS Secrets to Parameter Contexts</h3>
+
+ <p>
+ The AwsSecretsManagerParameterProvider maps a Secret to a Parameter
Context, with key/value pairs in the Secret
+ mapping to parameters. To create a compatible secret from the AWS
Console:
+ </p>
+
+ <ol>
+ <li>From the Secrets Manager service, click the "Store a new Secret"
button</li>
+ <li>Select "Other type of secret"</li>
+ <li>Under "Key/value", enter your parameters, with the parameter names
being the keys and the parameter values
+ being the values. Click Next.</li>
+ <li>Enter the Secret name. This will determine which Parameter
Context receives the parameters. Continue
+ through the rest of the wizard and finally click the "Store"
button.</li>
+ </ol>
+
+ <p>
+ Alternatively, from the command line, run a command like the following:
+ </p>
+
+ <pre>
+aws secretsmanager create-secret --name "[Context]" --secret-string '{
"[Param]": "[secretValue]", "[Param2]": "[secretValue2]" }'
+ </pre>
+
+ <p>In this example, [Context] should be the intended name of the Parameter
Context, [Param] and [Param2] should be
+ parameter names, and [secretValue] and [secretValue2] should be the values
of each respective parameter.</p>
+
+ <h3>Configuring the Parameter Provider</h3>
+
+ <p>
+ AWS Secrets must be explicitly matched in the "Secret Name Pattern"
property in order for them to be fetched. This
+ prevents more than the intended Secrets from being pulled into NiFi.
+ </p>
+
+ <p>
+
+ </p>
+</body>
+</html>
diff --git
a/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/test/java/org/apache/nifi/parameter/aws/TestAwsSecretsManagerParameterProvider.java
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/test/java/org/apache/nifi/parameter/aws/TestAwsSecretsManagerParameterProvider.java
new file mode 100644
index 0000000000..8b96a6e7f0
--- /dev/null
+++
b/nifi-nar-bundles/nifi-aws-bundle/nifi-aws-parameter-providers/src/test/java/org/apache/nifi/parameter/aws/TestAwsSecretsManagerParameterProvider.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.parameter.aws;
+
+import com.amazonaws.services.secretsmanager.AWSSecretsManager;
+import com.amazonaws.services.secretsmanager.model.AWSSecretsManagerException;
+import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest;
+import com.amazonaws.services.secretsmanager.model.GetSecretValueResult;
+import com.amazonaws.services.secretsmanager.model.ListSecretsResult;
+import com.amazonaws.services.secretsmanager.model.SecretListEntry;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterGroup;
+import org.apache.nifi.parameter.VerifiableParameterProvider;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.MockComponentLog;
+import org.apache.nifi.util.MockConfigurationContext;
+import org.apache.nifi.util.MockParameterProviderInitializationContext;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class TestAwsSecretsManagerParameterProvider {
+
+ @Mock
+ private AWSSecretsManager defaultSecretsManager;
+
+ @Mock
+ private ListSecretsResult listSecretsResult;
+
+ final ObjectMapper objectMapper = new ObjectMapper();
+
+ final List<Parameter> mySecretParameters = Arrays.asList(
+ parameter("paramA", "valueA"),
+ parameter("paramB", "valueB"),
+ parameter("otherC", "valueOther"),
+ parameter("paramD", "valueD"),
+ parameter("nonSensitiveE", "valueE"),
+ parameter("otherF", "valueF")
+ );
+ final List<Parameter> otherSecretParameters = Arrays.asList(
+ parameter("paramG", "valueG"),
+ parameter("otherH", "valueOther")
+ );
+ final List<ParameterGroup> mockParameterGroups = Arrays.asList(
+ new ParameterGroup("MySecret", mySecretParameters),
+ new ParameterGroup("OtherSecret", otherSecretParameters)
+ );
+
+ @Test
+ public void testFetchParametersWithNoSecrets() throws
InitializationException {
+ final List<ParameterGroup> expectedGroups =
Collections.singletonList(new ParameterGroup("MySecret",
Collections.emptyList()));
+ runProviderTest(mockSecretsManager(expectedGroups), 0,
ConfigVerificationResult.Outcome.SUCCESSFUL);
+ }
+
+ @Test
+ public void testFetchParameters() throws InitializationException {
+ runProviderTest(mockSecretsManager(mockParameterGroups), 8,
ConfigVerificationResult.Outcome.SUCCESSFUL);
+ }
+
+ @Test
+ public void testFetchParametersListFailure() throws
InitializationException {
+ when(defaultSecretsManager.listSecrets(any())).thenThrow(new
AWSSecretsManagerException("Fake exception"));
+ runProviderTest(defaultSecretsManager, 0,
ConfigVerificationResult.Outcome.FAILED);
+ }
+
+ @Test
+ public void testFetchParametersGetSecretFailure() throws
InitializationException {
+ final List<SecretListEntry> secretList = Collections.singletonList(new
SecretListEntry().withName("MySecret"));
+ when(listSecretsResult.getSecretList()).thenReturn(secretList);
+
when(defaultSecretsManager.listSecrets(any())).thenReturn(listSecretsResult);
+
when(defaultSecretsManager.getSecretValue(argThat(matchesGetSecretValueRequest("MySecret")))).thenThrow(new
AWSSecretsManagerException("Fake exception"));
+ runProviderTest(defaultSecretsManager, 0,
ConfigVerificationResult.Outcome.FAILED);
+ }
+
+ private AwsSecretsManagerParameterProvider getParameterProvider() {
+ return spy(new AwsSecretsManagerParameterProvider());
+ }
+
+ private AWSSecretsManager mockSecretsManager(final List<ParameterGroup>
mockGroup) {
+ final AWSSecretsManager secretsManager = mock(AWSSecretsManager.class);
+
+ final List<SecretListEntry> secretList = mockGroup.stream()
+ .map(group -> new
SecretListEntry().withName(group.getGroupName()))
+ .collect(Collectors.toList());
+ when(listSecretsResult.getSecretList()).thenReturn(secretList);
+ when(secretsManager.listSecrets(any())).thenReturn(listSecretsResult);
+
+ mockGroup.forEach(group -> {
+ final String groupName = group.getGroupName();
+ final Map<String, String> keyValues =
group.getParameters().stream().collect(Collectors.toMap(
+ param -> param.getDescriptor().getName(),
+ Parameter::getValue));
+ final String secretString;
+ try {
+ secretString = objectMapper.writeValueAsString(keyValues);
+ final GetSecretValueResult result = new
GetSecretValueResult().withName(groupName).withSecretString(secretString);
+
when(secretsManager.getSecretValue(argThat(matchesGetSecretValueRequest(groupName))))
+ .thenReturn(result);
+ } catch (final JsonProcessingException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ return secretsManager;
+ }
+
+ private List<ParameterGroup> runProviderTest(final AWSSecretsManager
secretsManager, final int expectedCount,
+ final
ConfigVerificationResult.Outcome expectedOutcome) throws
InitializationException {
+
+ final AwsSecretsManagerParameterProvider parameterProvider =
getParameterProvider();
+
doReturn(secretsManager).when(parameterProvider).configureClient(any());
+ final MockParameterProviderInitializationContext initContext = new
MockParameterProviderInitializationContext("id", "name",
+ new MockComponentLog("providerId", parameterProvider));
+ parameterProvider.initialize(initContext);
+
+ final Map<PropertyDescriptor, String> properties = new HashMap<>();
+ final MockConfigurationContext mockConfigurationContext = new
MockConfigurationContext(properties, null);
+
+ List<ParameterGroup> parameterGroups = new ArrayList<>();
+ // Verify parameter fetching
+ if (expectedOutcome == ConfigVerificationResult.Outcome.FAILED) {
+ assertThrows(RuntimeException.class, () ->
parameterProvider.fetchParameters(mockConfigurationContext));
+ } else {
+ parameterGroups =
parameterProvider.fetchParameters(mockConfigurationContext);
+ final int parameterCount = (int) parameterGroups.stream()
+ .flatMap(group -> group.getParameters().stream())
+ .count();
+ assertEquals(expectedCount, parameterCount);
+ }
+
+ // Verify config verification
+ final List<ConfigVerificationResult> results =
((VerifiableParameterProvider)
parameterProvider).verify(mockConfigurationContext, initContext.getLogger());
+
+ assertEquals(1, results.size());
+ assertEquals(expectedOutcome, results.get(0).getOutcome());
+
+ return parameterGroups;
+ }
+
+ private static Parameter parameter(final String name, final String value) {
+ return new Parameter(new
ParameterDescriptor.Builder().name(name).build(), value);
+ }
+
+ private static ArgumentMatcher<GetSecretValueRequest>
matchesGetSecretValueRequest(final String groupName) {
+ return new GetSecretValueRequestMatcher(groupName);
+ }
+
+ private static class GetSecretValueRequestMatcher implements
ArgumentMatcher<GetSecretValueRequest> {
+
+ private final String secretId;
+
+ private GetSecretValueRequestMatcher(final String secretId) {
+ this.secretId = secretId;
+ }
+
+ @Override
+ public boolean matches(final GetSecretValueRequest argument) {
+ return argument != null && argument.getSecretId().equals(secretId);
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-aws-bundle/pom.xml
b/nifi-nar-bundles/nifi-aws-bundle/pom.xml
index a6ab4fc31e..fc6a89e7e9 100644
--- a/nifi-nar-bundles/nifi-aws-bundle/pom.xml
+++ b/nifi-nar-bundles/nifi-aws-bundle/pom.xml
@@ -37,5 +37,6 @@
<module>nifi-aws-service-api-nar</module>
<module>nifi-aws-abstract-processors</module>
<module>nifi-aws-parameter-value-providers</module>
+ <module>nifi-aws-parameter-providers</module>
</modules>
</project>