This is an automated email from the ASF dual-hosted git repository.
kevdoran 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 09c6d7765a8 NIFI-15992 - Add secret caching bypass in Connector
user-called verify (#11306)
09c6d7765a8 is described below
commit 09c6d7765a8b34c267cfb57ed8222a115ec86682
Author: Pierre Villard <[email protected]>
AuthorDate: Wed Jun 3 06:05:09 2026 -0700
NIFI-15992 - Add secret caching bypass in Connector user-called verify
(#11306)
Signed-off-by: Kevin Doran <[email protected]>
---
.../secrets/ParameterProviderSecretsManager.java | 14 +++--
.../TestParameterProviderSecretsManager.java | 61 ++++++++++++++++++++++
.../connector/secrets/SecretsManager.java | 13 +++++
.../connector/StandardConnectorNode.java | 12 +++--
.../connector/TestStandardConnectorNode.java | 49 ++++++++++++++++-
5 files changed, 140 insertions(+), 9 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/components/connector/secrets/ParameterProviderSecretsManager.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/components/connector/secrets/ParameterProviderSecretsManager.java
index 5547170e979..6285ce9e16f 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/components/connector/secrets/ParameterProviderSecretsManager.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/components/connector/secrets/ParameterProviderSecretsManager.java
@@ -161,6 +161,11 @@ public class ParameterProviderSecretsManager implements
SecretsManager {
@Override
public Map<SecretReference, Secret> getSecrets(final Set<SecretReference>
secretReferences) {
+ return getSecrets(secretReferences, true);
+ }
+
+ @Override
+ public Map<SecretReference, Secret> getSecrets(final Set<SecretReference>
secretReferences, final boolean useCache) {
if (secretReferences.isEmpty()) {
return Map.of();
}
@@ -169,7 +174,7 @@ public class ParameterProviderSecretsManager implements
SecretsManager {
return fetchSecretsWithoutCache(secretReferences);
}
- return fetchSecretsWithCache(secretReferences);
+ return fetchSecretsWithCache(secretReferences, useCache);
}
private Map<SecretReference, Secret> fetchSecretsWithoutCache(final
Set<SecretReference> secretReferences) {
@@ -215,17 +220,18 @@ public class ParameterProviderSecretsManager implements
SecretsManager {
return secrets;
}
- private Map<SecretReference, Secret> fetchSecretsWithCache(final
Set<SecretReference> secretReferences) {
+ private Map<SecretReference, Secret> fetchSecretsWithCache(final
Set<SecretReference> secretReferences, final boolean useCache) {
final Set<SecretProvider> providers = getSecretProviders();
final Map<SecretReference, Secret> results = new HashMap<>();
- // Partition references into cache hits vs. misses that need fetching
+ // Partition references into cache hits vs. misses that need fetching.
When useCache is false, every
+ // reference is treated as a miss so fresh values are fetched and
written back, refreshing the cache.
final Map<SecretProvider, Set<SecretReference>> uncachedByProvider =
new HashMap<>();
for (final SecretReference secretReference : secretReferences) {
final String fqn = secretReference.getFullyQualifiedName();
- if (fqn != null) {
+ if (useCache && fqn != null) {
final CachedSecret cached = secretCache.get(fqn);
if (cached != null && !isExpired(cached)) {
logger.debug("Cached Secret found [{}]", fqn);
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/components/connector/secrets/TestParameterProviderSecretsManager.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/components/connector/secrets/TestParameterProviderSecretsManager.java
index 7ba784df077..b7dbd6895a6 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/components/connector/secrets/TestParameterProviderSecretsManager.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/components/connector/secrets/TestParameterProviderSecretsManager.java
@@ -127,6 +127,18 @@ public class TestParameterProviderSecretsManager {
return node;
}
+ private void updateSecretValue(final ParameterProviderNode node, final
String providerName, final String groupName, final String secretName, final
String newValue) {
+ when(node.fetchParameterValues(anyList())).thenAnswer(invocation -> {
+ final List<String> requestedNames = invocation.getArgument(0);
+ final String fullyQualifiedName = providerName + "." + groupName +
"." + secretName;
+ if (requestedNames.contains(fullyQualifiedName)) {
+ final Parameter updatedParameter = createParameter(secretName,
SECRET_1_DESCRIPTION, newValue);
+ return List.of(new ParameterGroup(groupName,
List.of(updatedParameter)));
+ }
+ return List.of();
+ });
+ }
+
private Parameter createParameter(final String name, final String
description, final String value) {
final ParameterDescriptor descriptor = new
ParameterDescriptor.Builder()
.name(name)
@@ -449,6 +461,55 @@ public class TestParameterProviderSecretsManager {
verify(providerNode, times(2)).fetchParameterValues(anyList());
}
+ @Test
+ public void testGetSecretsBypassingCacheRefetchesAndRefreshesCachedValue()
{
+ final ParameterProviderNode providerNode =
createMockedParameterProviderNode(PROVIDER_1_ID, PROVIDER_1_NAME, GROUP_1_NAME,
+ createParameter(SECRET_1_NAME, SECRET_1_DESCRIPTION,
SECRET_1_VALUE));
+ final ParameterProviderSecretsManager manager =
createManagerWithCacheDuration(DEFAULT_CACHE_DURATION, providerNode);
+
+ final SecretReference reference = createSecretReference(PROVIDER_1_ID,
null, SECRET_1_NAME);
+
+ final Map<SecretReference, Secret> cached =
manager.getSecrets(Set.of(reference));
+ assertEquals(SECRET_1_VALUE, cached.get(reference).getValue());
+ verify(providerNode, times(1)).fetchParameterValues(anyList());
+
+ final String updatedValue = "updated-secret-value-one";
+ updateSecretValue(providerNode, PROVIDER_1_NAME, GROUP_1_NAME,
SECRET_1_NAME, updatedValue);
+
+ // Bypassing the cache fetches the current value from the provider
rather than the cached value.
+ final Map<SecretReference, Secret> bypassed =
manager.getSecrets(Set.of(reference), false);
+ assertEquals(updatedValue, bypassed.get(reference).getValue());
+ verify(providerNode, times(2)).fetchParameterValues(anyList());
+
+ // The bypass refreshed the cache, so a subsequent cache-enabled read
returns the updated value without refetching.
+ final Map<SecretReference, Secret> afterRefresh =
manager.getSecrets(Set.of(reference), true);
+ assertEquals(updatedValue, afterRefresh.get(reference).getValue());
+ verify(providerNode, times(2)).fetchParameterValues(anyList());
+ }
+
+ @Test
+ public void testGetSecretsBypassingCacheLeavesOtherCachedEntriesIntact() {
+ final ParameterProviderNode providerNode =
createMockedParameterProviderNode(PROVIDER_1_ID, PROVIDER_1_NAME, GROUP_1_NAME,
+ createParameter(SECRET_1_NAME, SECRET_1_DESCRIPTION,
SECRET_1_VALUE),
+ createParameter(SECRET_2_NAME, SECRET_2_DESCRIPTION,
SECRET_2_VALUE));
+ final ParameterProviderSecretsManager manager =
createManagerWithCacheDuration(DEFAULT_CACHE_DURATION, providerNode);
+
+ final SecretReference reference1 =
createSecretReference(PROVIDER_1_ID, null, SECRET_1_NAME);
+ final SecretReference reference2 =
createSecretReference(PROVIDER_1_ID, null, SECRET_2_NAME);
+
+ manager.getSecrets(Set.of(reference1, reference2));
+ verify(providerNode, times(1)).fetchParameterValues(anyList());
+
+ // Bypassing the cache for reference1 only refetches reference1.
+ manager.getSecrets(Set.of(reference1), false);
+ verify(providerNode, times(2)).fetchParameterValues(anyList());
+
+ // reference2 remains cached and is served without an additional fetch.
+ final Map<SecretReference, Secret> reference2Result =
manager.getSecrets(Set.of(reference2), true);
+ assertEquals(SECRET_2_VALUE,
reference2Result.get(reference2).getValue());
+ verify(providerNode, times(2)).fetchParameterValues(anyList());
+ }
+
@Test
public void testGetSecretsBatchWithMixedCacheHitsAndMisses() {
final ParameterProviderNode providerNode =
createMockedParameterProviderNode(PROVIDER_1_ID, PROVIDER_1_NAME, GROUP_1_NAME,
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/secrets/SecretsManager.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/secrets/SecretsManager.java
index e3fbe297976..fccc59b3c45 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/secrets/SecretsManager.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/secrets/SecretsManager.java
@@ -37,6 +37,19 @@ public interface SecretsManager {
Map<SecretReference, Secret> getSecrets(Set<SecretReference>
secretReferences);
+ /**
+ * Resolves the given Secret References, optionally bypassing any cached
values. When {@code useCache} is
+ * {@code false}, implementations that cache Secret values must fetch
fresh values from the underlying
+ * providers and refresh their cache with the retrieved values, rather
than serving previously cached values.
+ *
+ * @param secretReferences the Secret References to resolve
+ * @param useCache whether cached values may be returned; when {@code
false}, values are fetched fresh and the cache is refreshed
+ * @return a mapping of each Secret Reference to its resolved Secret, or
{@code null} for references that cannot be resolved
+ */
+ default Map<SecretReference, Secret> getSecrets(final Set<SecretReference>
secretReferences, final boolean useCache) {
+ return getSecrets(secretReferences);
+ }
+
/**
* Invalidates any cached secret data, forcing the next access to fetch
fresh data
* from the underlying secret providers.
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
index b9659ebb486..2a82cfd962f 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
@@ -931,7 +931,9 @@ public class StandardConnectorNode implements ConnectorNode
{
final ConfigurationStep configurationStep = optionalStep.get();
final List<SecretReference> invalidSecretRefs = new ArrayList<>();
final List<AssetReference> invalidAssetRefs = new ArrayList<>();
- final Map<String, String> resolvedPropertyOverrides =
resolvePropertyReferences(configurationStep, configurationOverrides,
invalidSecretRefs, invalidAssetRefs);
+ // Bypass the Secret value cache during verification so the user
sees results based on the current
+ // Secret values rather than potentially stale cached values
awaiting TTL expiration.
+ final Map<String, String> resolvedPropertyOverrides =
resolvePropertyReferences(configurationStep, configurationOverrides,
invalidSecretRefs, invalidAssetRefs, false);
final DescribedValueProvider allowableValueProvider = (step,
propertyName) -> fetchAllowableValues(step, propertyName, workingFlowContext);
@@ -986,7 +988,7 @@ public class StandardConnectorNode implements ConnectorNode
{
}
private Map<String, String> resolvePropertyReferences(final
ConfigurationStep configurationStep, final StepConfiguration
configurationOverrides,
- final
List<SecretReference> invalidSecretRefs, final List<AssetReference>
invalidAssetRefs) {
+ final
List<SecretReference> invalidSecretRefs, final List<AssetReference>
invalidAssetRefs, final boolean useCache) {
final Map<String, String> resolvedProperties = new HashMap<>();
final Map<String, ConnectorPropertyDescriptor> descriptorLookup =
buildPropertyDescriptorLookup(configurationStep);
@@ -1008,7 +1010,7 @@ public class StandardConnectorNode implements
ConnectorNode {
final Map<SecretReference, Secret> secretsByReference =
secretReferences.isEmpty()
? Map.of()
- :
initializationContext.getSecretsManager().getSecrets(secretReferences);
+ :
initializationContext.getSecretsManager().getSecrets(secretReferences,
useCache);
secretsByReference.forEach((ref, secret) -> {
if (secret == null) {
invalidSecretRefs.add(ref);
@@ -1637,7 +1639,9 @@ public class StandardConnectorNode implements
ConnectorNode {
// Check for invalid Secret and Asset references
final List<SecretReference> invalidSecrets = new ArrayList<>();
final List<AssetReference> invalidAssets = new ArrayList<>();
- resolvePropertyReferences(step, stepConfiguration, invalidSecrets,
invalidAssets);
+ // Regular validation may run frequently, so cached Secret values
are used here to avoid
+ // repeatedly fetching from the underlying Secret Providers on
every validation cycle.
+ resolvePropertyReferences(step, stepConfiguration, invalidSecrets,
invalidAssets, true);
addInvalidReferenceResults(allResults, invalidSecrets,
invalidAssets);
}
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
index 00e136a6635..3c7c957cbbc 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
@@ -62,8 +62,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class TestStandardConnectorNode {
@@ -597,7 +601,7 @@ public class TestStandardConnectorNode {
// that stopped short-circuiting on unsatisfied dependencies would
surface the lookup attempt as a hard failure here
// instead of silently being masked by the empty-stub filter or by a
null-returning mock.
final SecretsManager strictSecretsManager = mock(SecretsManager.class);
- when(strictSecretsManager.getSecrets(anySet()))
+ when(strictSecretsManager.getSecrets(anySet(), anyBoolean()))
.thenThrow(new AssertionError("SecretsManager.getSecrets must not
be called when property dependencies are not satisfied"));
final DependentSecretVerifyConnector connector = new
DependentSecretVerifyConnector();
@@ -677,6 +681,48 @@ public class TestStandardConnectorNode {
}), () -> "Expected required-property failure in active validation,
got: " + errors);
}
+ @Test
+ public void testVerifyConfigurationStepResolvesSecretsBypassingCache()
throws FlowUpdateException {
+ final SecretsManager recordingSecretsManager =
mock(SecretsManager.class);
+ when(recordingSecretsManager.getSecrets(anySet(),
anyBoolean())).thenReturn(Collections.emptyMap());
+
+ final DependentSecretVerifyConnector connector = new
DependentSecretVerifyConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector, recordingSecretsManager);
+
+ connectorNode.transitionStateForUpdating();
+ connectorNode.prepareForUpdate();
+
+ final Map<String, ConnectorValueReference> propertyValues = new
HashMap<>();
+ propertyValues.put("Mode", new StringLiteralValue("WITH_SECRET"));
+ propertyValues.put("SecretKey", new SecretReference("pid", "My
Provider", "my-secret", "My Provider.my-secret"));
+
+ connectorNode.verifyConfigurationStep("authStep", new
StepConfiguration(propertyValues));
+
+ verify(recordingSecretsManager).getSecrets(anySet(), eq(false));
+ }
+
+ @Test
+ public void testPerformValidationResolvesSecretsUsingCache() throws
FlowUpdateException {
+ final SecretsManager recordingSecretsManager =
mock(SecretsManager.class);
+ when(recordingSecretsManager.getAllSecrets()).thenReturn(List.of());
+ when(recordingSecretsManager.getSecrets(anySet(),
anyBoolean())).thenReturn(Collections.emptyMap());
+
+ final DependentSecretVerifyConnector connector = new
DependentSecretVerifyConnector();
+ final StandardConnectorNode connectorNode =
createConnectorNode(connector, recordingSecretsManager);
+
+ connectorNode.transitionStateForUpdating();
+ connectorNode.prepareForUpdate();
+ final Map<String, ConnectorValueReference> propertyValues = new
HashMap<>();
+ propertyValues.put("Mode", new StringLiteralValue("WITH_SECRET"));
+ propertyValues.put("SecretKey", new SecretReference("pid", "My
Provider", "my-secret", "My Provider.my-secret"));
+ connectorNode.setConfiguration("authStep", new
StepConfiguration(propertyValues));
+ connectorNode.applyUpdate();
+
+ connectorNode.performValidation();
+
+ verify(recordingSecretsManager, atLeastOnce()).getSecrets(anySet(),
eq(true));
+ }
+
@Test
@Timeout(value = 5, unit = TimeUnit.SECONDS)
public void testDrainFlowFilesTransitionsStateToDraining() throws
FlowUpdateException {
@@ -792,6 +838,7 @@ public class TestStandardConnectorNode {
final SecretsManager defaultSecretsManager =
mock(SecretsManager.class);
when(defaultSecretsManager.getAllSecrets()).thenReturn(List.of());
when(defaultSecretsManager.getSecrets(anySet())).thenReturn(Collections.emptyMap());
+ when(defaultSecretsManager.getSecrets(anySet(),
anyBoolean())).thenReturn(Collections.emptyMap());
return createConnectorNode(connector, defaultSecretsManager);
}