NIFI-9266 Added Azure Key Vault Secret SPP
Signed-off-by: Pierre Villard <pierre.villard.fr@gmail.com>
This closes #5435.
diff --git a/nifi-commons/nifi-sensitive-property-provider/pom.xml b/nifi-commons/nifi-sensitive-property-provider/pom.xml
index 5ef4a43..0fb83e35 100644
--- a/nifi-commons/nifi-sensitive-property-provider/pom.xml
+++ b/nifi-commons/nifi-sensitive-property-provider/pom.xml
@@ -73,8 +73,31 @@
</dependency>
<dependency>
<groupId>com.azure</groupId>
+ <artifactId>azure-security-keyvault-secrets</artifactId>
+ <version>4.3.3</version>
+ <exclusions>
+ <exclusion>
+ <groupId>com.azure</groupId>
+ <artifactId>azure-core-http-netty</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.woodstox</groupId>
+ <artifactId>woodstox-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.codehaus.woodstox</groupId>
+ <artifactId>stax2-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-tcnative-boringssl-static</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-keys</artifactId>
- <version>4.3.1</version>
+ <version>4.3.3</version>
<exclusions>
<exclusion>
<groupId>com.azure</groupId>
@@ -144,6 +167,12 @@
<version>2.10.0</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-inline</artifactId>
+ <version>${mockito.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<!-- Required to run Groovy tests without any Java tests -->
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProvider.java
new file mode 100644
index 0000000..ac9e1da
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProvider.java
@@ -0,0 +1,125 @@
+/*
+ * 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.properties;
+
+import com.azure.core.exception.ResourceNotFoundException;
+import com.azure.security.keyvault.secrets.SecretClient;
+import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
+
+import java.util.Objects;
+
+/**
+ * Microsoft Azure Key Vault Secret implementation of Sensitive Property Provider for externalized storage of properties
+ */
+public class AzureKeyVaultSecretSensitivePropertyProvider implements SensitivePropertyProvider {
+ private static final String FORWARD_SLASH = "/";
+
+ private static final String PERIOD = "\\.";
+
+ private static final String DASH = "-";
+
+ private SecretClient secretClient;
+
+ AzureKeyVaultSecretSensitivePropertyProvider(final SecretClient secretClient) {
+ this.secretClient = secretClient;
+ }
+
+ /**
+ * Get Provider name using Protection Scheme
+ *
+ * @return Provider name
+ */
+ @Override
+ public String getName() {
+ return PropertyProtectionScheme.AZURE_KEYVAULT_SECRET.getName();
+ }
+
+ /**
+ * Get Identifier key using Protection Scheme
+ *
+ * @return Identifier key
+ */
+ @Override
+ public String getIdentifierKey() {
+ return PropertyProtectionScheme.AZURE_KEYVAULT_SECRET.getIdentifier();
+ }
+
+ /**
+ * Is Provider supported returns status based on configuration of Secret Client
+ *
+ * @return Supported status
+ */
+ @Override
+ public boolean isSupported() {
+ return secretClient != null;
+ }
+
+ /**
+ * Protect value stores a Secret in Azure Key Vault using the Property Context Key as the Secret Name
+ *
+ * @param unprotectedValue Value to be stored
+ * @param context Property Context containing Context Key used to store Secret
+ * @return Key Vault Secret Identifier
+ * @throws SensitivePropertyProtectionException Thrown when storing Secret failed
+ */
+ @Override
+ public String protect(final String unprotectedValue, final ProtectedPropertyContext context) throws SensitivePropertyProtectionException {
+ Objects.requireNonNull(unprotectedValue, "Value required");
+ final String secretName = getSecretName(context);
+ try {
+ final KeyVaultSecret keyVaultSecret = secretClient.setSecret(secretName, unprotectedValue);
+ return keyVaultSecret.getId();
+ } catch (final RuntimeException e) {
+ throw new SensitivePropertyProtectionException(String.format("Set Secret [%s] failed", secretName), e);
+ }
+ }
+
+ /**
+ * Unprotect value retrieves a Secret from Azure Key Vault using Property Context Key
+ *
+ * @param protectedValue Key Vault Secret Identifier is not used
+ * @param context Property Context containing Context Key used to retrieve Secret
+ * @return Secret Value
+ * @throws SensitivePropertyProtectionException Thrown when Secret not found or retrieval failed
+ */
+ @Override
+ public String unprotect(final String protectedValue, final ProtectedPropertyContext context) throws SensitivePropertyProtectionException {
+ final String secretName = getSecretName(context);
+ try {
+ final KeyVaultSecret keyVaultSecret = secretClient.getSecret(secretName);
+ return keyVaultSecret.getValue();
+ } catch (final ResourceNotFoundException e) {
+ throw new SensitivePropertyProtectionException(String.format("Secret [%s] not found", secretName), e);
+ } catch (final RuntimeException e) {
+ throw new SensitivePropertyProtectionException(String.format("Secret [%s] request failed", secretName), e);
+ }
+ }
+
+ /**
+ * Clean up not implemented
+ */
+ @Override
+ public void cleanUp() {
+
+ }
+
+ private String getSecretName(final ProtectedPropertyContext context) {
+ final String contextKey = Objects.requireNonNull(context, "Context required").getContextKey();
+ // Replace forward slash and period with dash since Azure Key Vault Secret Names do not support certain characters
+ return contextKey.replaceAll(FORWARD_SLASH, DASH).replaceAll(PERIOD, DASH);
+ }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
index 157fc81..7d71004 100644
--- a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
@@ -27,6 +27,7 @@
AES_GCM("aes/gcm/(128|192|256)", "aes/gcm/%s", "AES Sensitive Property Provider", true),
AWS_KMS("aws/kms", "aws/kms", "AWS KMS Sensitive Property Provider", false),
AZURE_KEYVAULT_KEY("azure/keyvault/key", "azure/keyvault/key", "Azure Key Vault Key Sensitive Property Provider", false),
+ AZURE_KEYVAULT_SECRET("azure/keyvault/secret", "azure/keyvault/secret", "Azure Key Vault Secret Sensitive Property Provider", false),
GCP_KMS("gcp/kms", "gcp/kms", "GCP Cloud KMS Sensitive Property Provider", false),
HASHICORP_VAULT_KV("hashicorp/vault/kv/[a-zA-Z0-9_-]+", "hashicorp/vault/kv/%s", "HashiCorp Vault Key/Value Engine Sensitive Property Provider", false),
HASHICORP_VAULT_TRANSIT("hashicorp/vault/transit/[a-zA-Z0-9_-]+", "hashicorp/vault/transit/%s", "HashiCorp Vault Transit Engine Sensitive Property Provider", false);
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
index 1564e3f..1d7e12f 100644
--- a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
@@ -17,10 +17,12 @@
package org.apache.nifi.properties;
import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
+import com.azure.security.keyvault.secrets.SecretClient;
import com.google.cloud.kms.v1.KeyManagementServiceClient;
import org.apache.nifi.properties.BootstrapProperties.BootstrapPropertyKey;
import org.apache.nifi.properties.configuration.AwsKmsClientProvider;
import org.apache.nifi.properties.configuration.AzureCryptographyClientProvider;
+import org.apache.nifi.properties.configuration.AzureSecretClientProvider;
import org.apache.nifi.properties.configuration.ClientProvider;
import org.apache.nifi.properties.configuration.GoogleKeyManagementServiceClientProvider;
import org.apache.nifi.util.NiFiBootstrapUtils;
@@ -142,6 +144,13 @@
final Optional<CryptographyClient> cryptographyClient = clientProvider.getClient(clientProperties);
return new AzureKeyVaultKeySensitivePropertyProvider(cryptographyClient.orElse(null), clientProperties);
});
+ case AZURE_KEYVAULT_SECRET:
+ return providerMap.computeIfAbsent(protectionScheme, s -> {
+ final AzureSecretClientProvider clientProvider = new AzureSecretClientProvider();
+ final Properties clientProperties = getClientProperties(clientProvider);
+ final Optional<SecretClient> secretClient = clientProvider.getClient(clientProperties);
+ return new AzureKeyVaultSecretSensitivePropertyProvider(secretClient.orElse(null));
+ });
case GCP_KMS:
return providerMap.computeIfAbsent(protectionScheme, s -> {
final GoogleKeyManagementServiceClientProvider clientProvider = new GoogleKeyManagementServiceClientProvider();
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureClientProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureClientProvider.java
new file mode 100644
index 0000000..61561e7
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureClientProvider.java
@@ -0,0 +1,68 @@
+/*
+ * 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.properties.configuration;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
+import org.apache.nifi.properties.BootstrapProperties;
+import org.apache.nifi.util.StringUtils;
+
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * Abstract Microsoft Azure Client Provider
+ */
+public abstract class AzureClientProvider<T> extends BootstrapPropertiesClientProvider<T> {
+ public AzureClientProvider() {
+ super(BootstrapProperties.BootstrapPropertyKey.AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF);
+ }
+
+ /**
+ * Get Client using Client Properties
+ *
+ * @param clientProperties Client Properties can be null
+ * @return Configured Client or empty when Client Properties object is null
+ */
+ @Override
+ public Optional<T> getClient(final Properties clientProperties) {
+ return isMissingProperties(clientProperties) ? Optional.empty() : Optional.of(getConfiguredClient(clientProperties));
+ }
+
+ /**
+ * Get Default Azure Token Credential using Default Credentials Builder for environment variables and system properties
+ *
+ * @return Token Credential
+ */
+ protected TokenCredential getDefaultTokenCredential() {
+ return new DefaultAzureCredentialBuilder().build();
+ }
+
+ /**
+ * Get Property Names required for initializing client in order to perform initial validation
+ *
+ * @return Set of required client property names
+ */
+ protected abstract Set<String> getRequiredPropertyNames();
+
+ private boolean isMissingProperties(final Properties clientProperties) {
+ return clientProperties == null || getRequiredPropertyNames().stream().anyMatch(propertyName ->
+ StringUtils.isBlank(clientProperties.getProperty(propertyName))
+ );
+ }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java
index 643d02e..adf974e 100644
--- a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProvider.java
@@ -16,23 +16,22 @@
*/
package org.apache.nifi.properties.configuration;
-import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
import com.azure.security.keyvault.keys.cryptography.CryptographyClientBuilder;
-import org.apache.nifi.properties.BootstrapProperties;
import org.apache.nifi.properties.SensitivePropertyProtectionException;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.Properties;
+import java.util.Set;
/**
* Microsoft Azure Cryptography Client Provider
*/
-public class AzureCryptographyClientProvider extends BootstrapPropertiesClientProvider<CryptographyClient> {
- private static final String KEY_ID_PROPERTY = "azure.keyvault.key.id";
+public class AzureCryptographyClientProvider extends AzureClientProvider<CryptographyClient> {
+ protected static final String KEY_ID_PROPERTY = "azure.keyvault.key.id";
- public AzureCryptographyClientProvider() {
- super(BootstrapProperties.BootstrapPropertyKey.AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF);
- }
+ private static final Set<String> REQUIRED_PROPERTY_NAMES = new HashSet<>(Collections.singletonList(KEY_ID_PROPERTY));
/**
* Get Configured Client using Default Azure Credentials Builder and configured Key Identifier
@@ -47,11 +46,21 @@
try {
return new CryptographyClientBuilder()
- .credential(new DefaultAzureCredentialBuilder().build())
+ .credential(getDefaultTokenCredential())
.keyIdentifier(keyIdentifier)
.buildClient();
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("Azure Cryptography Builder Client Failed using Default Credentials", e);
}
}
+
+ /**
+ * Get required property names for Azure Cryptography Client
+ *
+ * @return Required client property names
+ */
+ @Override
+ protected Set<String> getRequiredPropertyNames() {
+ return REQUIRED_PROPERTY_NAMES;
+ }
}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureSecretClientProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureSecretClientProvider.java
new file mode 100644
index 0000000..65432fa
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/configuration/AzureSecretClientProvider.java
@@ -0,0 +1,69 @@
+/*
+ * 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.properties.configuration;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
+import com.azure.security.keyvault.secrets.SecretClient;
+import com.azure.security.keyvault.secrets.SecretClientBuilder;
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * Microsoft Azure Secret Client Provider
+ */
+public class AzureSecretClientProvider extends AzureClientProvider<SecretClient> {
+ protected static final String URI_PROPERTY = "azure.keyvault.uri";
+
+ private static final Set<String> REQUIRED_PROPERTY_NAMES = new HashSet<>(Collections.singletonList(URI_PROPERTY));
+
+ /**
+ * Get Secret Client using Default Azure Credentials Builder and default configuration from environment variables
+ *
+ * @param clientProperties Client Properties
+ * @return Secret Client
+ */
+ @Override
+ protected SecretClient getConfiguredClient(final Properties clientProperties) {
+ final String uri = clientProperties.getProperty(URI_PROPERTY);
+ logger.debug("Azure Secret Client with URI [{}]", uri);
+
+ try {
+ final TokenCredential credential = new DefaultAzureCredentialBuilder().build();
+ return new SecretClientBuilder()
+ .credential(credential)
+ .vaultUrl(uri)
+ .buildClient();
+ } catch (final RuntimeException e) {
+ throw new SensitivePropertyProtectionException("Azure Secret Builder Client Failed using Default Credentials", e);
+ }
+ }
+
+ /**
+ * Get required property names for Azure Secret Client
+ *
+ * @return Required client property names
+ */
+ @Override
+ protected Set<String> getRequiredPropertyNames() {
+ return REQUIRED_PROPERTY_NAMES;
+ }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProviderTest.java b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProviderTest.java
new file mode 100644
index 0000000..3ec2d4a
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/AzureKeyVaultSecretSensitivePropertyProviderTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.properties;
+
+import com.azure.core.exception.AzureException;
+import com.azure.core.exception.ResourceNotFoundException;
+import com.azure.core.http.HttpResponse;
+import com.azure.security.keyvault.secrets.SecretClient;
+import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class AzureKeyVaultSecretSensitivePropertyProviderTest {
+ private static final String PROPERTY_NAME = "property.name";
+
+ private static final String SECRET_NAME = "default-property-name";
+
+ private static final String PROPERTY = String.class.getName();
+
+ private static final String PROTECTED_PROPERTY = KeyVaultSecret.class.getSimpleName();
+
+ private static final String ID = KeyVaultSecret.class.getName();
+
+ @Mock
+ private SecretClient secretClient;
+
+ @Mock
+ private KeyVaultSecret keyVaultSecret;
+
+ @Mock
+ private HttpResponse httpResponse;
+
+ private AzureKeyVaultSecretSensitivePropertyProvider provider;
+
+ @BeforeEach
+ public void setProvider() {
+ provider = new AzureKeyVaultSecretSensitivePropertyProvider(secretClient);
+ }
+
+ @Test
+ public void testClientNull() {
+ final AzureKeyVaultSecretSensitivePropertyProvider provider = new AzureKeyVaultSecretSensitivePropertyProvider(null);
+ assertNotNull(provider);
+ assertFalse(provider.isSupported());
+ }
+
+ @Test
+ public void testProtect() {
+ when(secretClient.setSecret(eq(SECRET_NAME), eq(PROPERTY))).thenReturn(keyVaultSecret);
+ when(keyVaultSecret.getId()).thenReturn(ID);
+
+ final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+ final String protectedProperty = provider.protect(PROPERTY, context);
+ assertEquals(ID, protectedProperty);
+ }
+
+ @Test
+ public void testProtectException() {
+ final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+ final String secretName = context.getContextKey();
+ when(secretClient.setSecret(eq(secretName), eq(PROPERTY))).thenThrow(new AzureException());
+
+ assertThrows(SensitivePropertyProtectionException.class, () -> provider.protect(PROPERTY, context));
+ }
+
+ @Test
+ public void testUnprotect() {
+ when(secretClient.getSecret(eq(SECRET_NAME))).thenReturn(keyVaultSecret);
+ when(keyVaultSecret.getValue()).thenReturn(PROPERTY);
+
+ final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+ final String property = provider.unprotect(PROTECTED_PROPERTY, context);
+ assertEquals(PROPERTY, property);
+ }
+
+ @Test
+ public void testUnprotectResourceNotFoundException() {
+ when(secretClient.getSecret(eq(SECRET_NAME))).thenThrow(new ResourceNotFoundException(SECRET_NAME, httpResponse));
+
+ final ProtectedPropertyContext context = ProtectedPropertyContext.defaultContext(PROPERTY_NAME);
+ assertThrows(SensitivePropertyProtectionException.class, () -> provider.unprotect(PROTECTED_PROPERTY, context));
+ }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProviderTest.java b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProviderTest.java
new file mode 100644
index 0000000..e898ea4
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureCryptographyClientProviderTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.properties.configuration;
+
+import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
+import org.apache.nifi.util.StringUtils;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class AzureCryptographyClientProviderTest {
+
+ @Test
+ public void testClientPropertiesNull() {
+ final AzureCryptographyClientProvider provider = new AzureCryptographyClientProvider();
+ final Optional<CryptographyClient> optionalClient = provider.getClient(null);
+ assertFalse(optionalClient.isPresent());
+ }
+
+ @Test
+ public void testClientPropertiesKeyIdBlank() {
+ final AzureCryptographyClientProvider provider = new AzureCryptographyClientProvider();
+ final Properties clientProperties = new Properties();
+ clientProperties.setProperty(AzureCryptographyClientProvider.KEY_ID_PROPERTY, StringUtils.EMPTY);
+ final Optional<CryptographyClient> optionalClient = provider.getClient(clientProperties);
+ assertFalse(optionalClient.isPresent());
+ }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureSecretClientProviderTest.java b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureSecretClientProviderTest.java
new file mode 100644
index 0000000..6f312df
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/configuration/AzureSecretClientProviderTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.properties.configuration;
+
+import com.azure.security.keyvault.secrets.SecretClient;
+import org.apache.nifi.util.StringUtils;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class AzureSecretClientProviderTest {
+
+ @Test
+ public void testClientPropertiesNull() {
+ final AzureSecretClientProvider provider = new AzureSecretClientProvider();
+ final Optional<SecretClient> optionalClient = provider.getClient(null);
+ assertFalse(optionalClient.isPresent());
+ }
+
+ @Test
+ public void testClientPropertiesUriBlank() {
+ final AzureSecretClientProvider provider = new AzureSecretClientProvider();
+ final Properties clientProperties = new Properties();
+ clientProperties.setProperty(AzureSecretClientProvider.URI_PROPERTY, StringUtils.EMPTY);
+ final Optional<SecretClient> optionalClient = provider.getClient(clientProperties);
+ assertFalse(optionalClient.isPresent());
+ }
+}
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 4f64041..8d2c58e 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -1858,8 +1858,21 @@
|`aws.secret.access.key`|The secret access key used to access AWS KMS.|_none_
|===
-=== Azure Key Vault Key provider
-This protection scheme uses keys managed by https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption. Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. The provider will use the Azure default credentials provider chain as described in the https://docs.microsoft.com/en-us/java/api/overview/azure/security-keyvault-keys-readme?view=azure-java-stable[Azure Key Vault Key client library for Java] documentation.
+[[azure-key-vault-key-provider]]
+=== Azure Key Vault Key Provider
+This protection scheme uses keys managed by
+https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption.
+
+Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the
+`bootstrap.conf` of NiFi or NiFi Registry.
+The provider will use the
+https://docs.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential[DefaultAzureCredential]
+for authentication.
+The https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#key-concepts[Azure Identity] client library
+describes the process for credentials resolution, which leverages environment variables, system properties, and falls
+back to
+https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#managed-identity-support[Managed Identity]
+authentication.
==== Required properties
[options="header,footer"]
@@ -1869,6 +1882,39 @@
|`azure.keyvault.encryption.algorithm`|The encryption algorithm that the Azure Key Vault client uses for encryption and decryption.|_none_
|===
+[[azure-key-vault-secret-provider]]
+=== Azure Key Vault Secret Provider
+This protection scheme uses secrets managed by
+https://docs.microsoft.com/en-us/azure/key-vault/secrets/about-secrets[Azure Key Vault Secrets] for storing and
+retrieving protected properties.
+
+Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the
+`bootstrap.conf` of NiFi or NiFi Registry.
+The provider will use the
+https://docs.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential[DefaultAzureCredential]
+for authentication.
+The https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#key-concepts[Azure Identity] client library
+describes the process for credentials resolution, which leverages environment variables, system properties, and falls
+back to
+https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#managed-identity-support[Managed Identity]
+authentication.
+
+Names of secrets stored in Azure Key Vault support alphanumeric and dash characters, but do not support characters such as `/` or `.`.
+For this reason, NiFi replaces these characters with `-` when storing and retrieving secrets. The following table provides an example property name mapping:
+
+[options="header,footer"]
+|===
+|Property Context|Property Name|Secret Name
+|`default`|`nifi.security.keystorePasswd`|`default-nifi-security-keystorePasswd`
+|===
+
+==== Required properties
+[options="header,footer"]
+|===
+|Property Name|Description|Default
+|`azure.keyvault.uri`|URI for the Azure Key Vault service such as `https://{value-name}.vault.azure.net/` |_none_
+|===
+
=== Google Cloud KMS provider
This protection scheme uses Google Cloud Key Management Service (https://cloud.google.com/security-key-management[Google Cloud Key Management Service]) for encryption and decryption. Google Cloud KMS configuration properties are to be stored in the `bootstrap-gcp.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. Credentials must be configured as per the following documentation: https://cloud.google.com/kms/docs/reference/libraries[Google Cloud KMS documentation]
diff --git a/nifi-docs/src/main/asciidoc/toolkit-guide.adoc b/nifi-docs/src/main/asciidoc/toolkit-guide.adoc
index be4eb79..d1c74d9 100644
--- a/nifi-docs/src/main/asciidoc/toolkit-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/toolkit-guide.adoc
@@ -435,7 +435,7 @@
* `-f`,`--flowXml <file>` The _flow.xml.gz_ file currently protected with old password (will be overwritten unless `-g` is specified)
* `-g`,`--outputFlowXml <file>` The destination _flow.xml.gz_ file containing protected config values (will not modify input _flow.xml.gz_)
* `-b`,`--bootstrapConf <file>` The bootstrap.conf file to persist root key and to optionally provide any configuration for the protection scheme.
-* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<GCP_KMS>>] (default is AES_GCM)
+* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>] (default is AES_GCM)
* `-k`,`--key <keyhex>` The raw hexadecimal key to use to encrypt the sensitive properties
* `-e`,`--oldKey <keyhex>` The old raw hexadecimal key to use during key migration
* `-H`,`--oldProtectionScheme <protectionScheme>` The old protection scheme to use during encryption migration (see --protectionScheme for possible values). Default is AES_GCM
@@ -456,7 +456,7 @@
* `-v`,`--verbose` Sets verbose mode (default false)
* `-p`,`--password <password>` Protect the files using a password-derived key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the password.
* `-k`,`--key <keyhex>` Protect the files using a raw hexadecimal key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the key.
-* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<GCP_KMS>>] (default is AES_GCM)
+* `-S`,`--protectionScheme <protectionScheme>` Selects the protection scheme for encrypted properties. Valid values are: [<<AES_GCM>>, <<HASHICORP_VAULT_TRANSIT>>, <<HASHICORP_VAULT_KV>>, <<AWS_KMS>>, <<AZURE_KEYVAULT_KEY>>, <<AZURE_KEYVAULT_SECRET>>, <<GCP_KMS>>] (default is AES_GCM)
* `--oldPassword <password>` If the input files are already protected using a password-derived key, this specifies the old password so that the files can be unprotected before re-protecting.
* `--oldKey <keyhex>` If the input files are already protected using a key, this specifies the raw hexadecimal key so that the files can be unprotected before re-protecting.
* `-H`,`--oldProtectionScheme <protectionScheme>`The old protection scheme to use during encryption migration (see --protectionScheme for possible values). Default is AES_GCM.
@@ -485,8 +485,40 @@
==== AWS_KMS [[AWS_KMS]]
This protection scheme uses https://aws.amazon.com/kms/[AWS Key Management] Service for encryption and decryption. AWS KMS configuration properties can be stored in the `bootstrap-aws.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. If the configuration properties are not specified in `bootstrap-aws.conf`, then the provider will attempt to use the AWS default credentials provider, which checks standard environment variables and system properties. Therefore, when using the AWS_KMS protection scheme, the `nifi(.registry)?.bootstrap.protection.aws.kms.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_aws_kms_provider, AWS KMS provider>> section in the link:administration-guide.html[NiFi Administration Guide].
-==== AZURE_KEYVAULT_KEY [[AZURE_KEYVAULT_KEY]]
-This protection scheme uses keys managed by https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption. Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. The provider will utilize the Azure default credentials provider chain as described in the https://docs.microsoft.com/en-us/java/api/overview/azure/security-keyvault-keys-readme?view=azure-java-stable[Azure Key Vault Key client library for Java] documentation. Therefore, when using the AZURE_KEYVAULT_KEY protection scheme, the `nifi(.registry)?.bootstrap.protection.azure.keyvault.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_azure_key_vault_key_provider, Azure Key Vault Key provider>> section in the link:administration-guide.html[NiFi Administration Guide].
+==== Microsoft Azure Key Vault Sensitive Property Providers
+
+Azure Key Vault configuration properties can be stored in the `bootstrap-azure.conf` file, as referenced in the
+`bootstrap.conf` of NiFi or NiFi Registry.
+
+Azure Key Vault providers will use the
+https://docs.microsoft.com/en-us/java/api/com.azure.identity.defaultazurecredential[DefaultAzureCredential]
+for authentication.
+The https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#key-concepts[Azure Identity] client library
+describes the process for credentials resolution, which leverages environment variables, system properties, and falls
+back to
+https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme#managed-identity-support[Managed Identity]
+authentication.
+
+When using Azure Key Vault providers, `bootstrap.conf` must contain the
+`nifi.bootstrap.protection.azure.keyvault.conf` property. The `bootstrap.conf` file location must be specified using the
+`-b` argument when running the Encrypt Config Tool.
+
+===== AZURE_KEYVAULT_KEY [[AZURE_KEYVAULT_KEY]]
+
+This protection scheme uses keys managed by
+https://docs.microsoft.com/en-us/azure/key-vault/keys/about-keys[Azure Key Vault Keys] for encryption and decryption.
+
+See <<administration-guide.adoc#azure-key-vault-key-provider,Azure Key Vault Key Provider>> in the NiFi System
+Administrator's Guide for required properties.
+
+===== AZURE_KEYVAULT_SECRET [[AZURE_KEYVAULT_SECRET]]
+
+This protection scheme uses secrets managed by
+https://docs.microsoft.com/en-us/azure/key-vault/secrets/about-secrets[Azure Key Vault Secrets] for storing and
+retrieving sensitive property values.
+
+See <<administration-guide.adoc#azure-key-vault-secret-provider,Azure Key Vault Secret Provider>> in the NiFi System
+Administrator's Guide for required properties.
==== GCP_KMS [[GCP_KMS]]
This protection scheme uses Google Cloud Key Management Service (https://cloud.google.com/security-key-management[Google Cloud Key Management Service]) for encryption and decryption. Google Cloud KMS configuration properties are to be stored in the `bootstrap-gcp.conf` file, as referenced in the `bootstrap.conf` of NiFi or NiFi Registry. Credentials must be configured as per the following documentation: https://cloud.google.com/kms/docs/reference/libraries[Google Cloud KMS documentation]. Therefore, when using the GCP_KMS protection scheme, the `nifi(.registry)?.bootstrap.protection.gcp.kms.conf` property in the `bootstrap.conf` specified using the `-b` flag must be available to the Encrypt Configuration Tool and must be configured as described in the <<administration-guide.adoc#_google_cloud_kms_provider,Google Cloud KMS provider>> section in the link:administration-guide.html[NiFi Administration Guide].
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf
index f7cc47e..49e318e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap-azure.conf
@@ -15,6 +15,10 @@
# limitations under the License.
#
-# Azure KeyVault Key ID and Encryption Algorithm is required to be configured for Azure KeyVault Sensitive Property Provider
+# Key Identifier for Azure Key Vault Key Sensitive Property Provider
azure.keyvault.key.id=
-azure.keyvault.encryption.algorithm=
\ No newline at end of file
+# Encryption Algorithm for Azure Key Vault Key Sensitive Property Provider
+azure.keyvault.encryption.algorithm=
+
+# Vault URI for Azure Key Vault Secret Sensitive Property Provider
+azure.keyvault.uri=
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf
index f7cc47e..49e318e 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf
+++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap-azure.conf
@@ -15,6 +15,10 @@
# limitations under the License.
#
-# Azure KeyVault Key ID and Encryption Algorithm is required to be configured for Azure KeyVault Sensitive Property Provider
+# Key Identifier for Azure Key Vault Key Sensitive Property Provider
azure.keyvault.key.id=
-azure.keyvault.encryption.algorithm=
\ No newline at end of file
+# Encryption Algorithm for Azure Key Vault Key Sensitive Property Provider
+azure.keyvault.encryption.algorithm=
+
+# Vault URI for Azure Key Vault Secret Sensitive Property Provider
+azure.keyvault.uri=
diff --git a/pom.xml b/pom.xml
index 54ebe3c..2fc31dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,6 +111,7 @@
<aspectj.version>1.9.6</aspectj.version>
<jersey.version>2.33</jersey.version>
<logback.version>1.2.6</logback.version>
+ <mockito.version>2.28.2</mockito.version>
</properties>
<repositories>
@@ -286,12 +287,12 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
- <version>2.28.2</version>
+ <version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
- <version>2.28.2</version>
+ <version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>