This is an automated email from the ASF dual-hosted git repository.

pvillard 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 8d0eec2  NIFI-9266 Added Azure Key Vault Secret SPP
8d0eec2 is described below

commit 8d0eec2d620d994c77c01388e7df9cf2a227c3c9
Author: exceptionfactory <[email protected]>
AuthorDate: Fri Oct 1 16:34:09 2021 -0500

    NIFI-9266 Added Azure Key Vault Secret SPP
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #5435.
---
 .../nifi-sensitive-property-provider/pom.xml       |  31 ++++-
 ...ureKeyVaultSecretSensitivePropertyProvider.java | 125 +++++++++++++++++++++
 .../nifi/properties/PropertyProtectionScheme.java  |   1 +
 .../StandardSensitivePropertyProviderFactory.java  |   9 ++
 .../configuration/AzureClientProvider.java         |  68 +++++++++++
 .../AzureCryptographyClientProvider.java           |  25 +++--
 .../configuration/AzureSecretClientProvider.java   |  69 ++++++++++++
 ...eyVaultSecretSensitivePropertyProviderTest.java | 108 ++++++++++++++++++
 .../AzureCryptographyClientProviderTest.java       |  45 ++++++++
 .../AzureSecretClientProviderTest.java             |  45 ++++++++
 .../src/main/asciidoc/administration-guide.adoc    |  50 ++++++++-
 nifi-docs/src/main/asciidoc/toolkit-guide.adoc     |  40 ++++++-
 .../src/main/resources/conf/bootstrap-azure.conf   |   8 +-
 .../src/main/resources/conf/bootstrap-azure.conf   |   8 +-
 pom.xml                                            |   5 +-
 15 files changed, 616 insertions(+), 21 deletions(-)

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 @@ public enum PropertyProtectionScheme {
     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 @@ public class StandardSensitivePropertyProviderFactory 
implements SensitiveProper
                     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 @@ public class AzureCryptographyClientProvider extends 
BootstrapPropertiesClientPr
 
         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 @@ This provider uses https://aws.amazon.com/kms/[AWS Key 
Management Service] for d
 |`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?v
 [...]
+[[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 @@ This protection scheme uses keys managed by 
https://docs.microsoft.com/en-us/azu
 |`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 @@ The following are available options when targeting NiFi:
 * `-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 @@ The following are available options when targeting NiFi 
Registry using the `--ni
 * `-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 @@ This protection scheme uses 
https://www.vaultproject.io/docs/secrets/kv/kv-v1[Ha
 ==== 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, wh [...]
 
-==== 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-read
 [...]
+==== 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] [...]
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>

Reply via email to