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

markap14 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 2b383e1ac38 NIFI-15910 Improve Connector Secret verification (#11210)
2b383e1ac38 is described below

commit 2b383e1ac383330460bb68829c2e84046c0f94e2
Author: Kevin Doran <[email protected]>
AuthorDate: Wed May 6 15:31:03 2026 -0400

    NIFI-15910 Improve Connector Secret verification (#11210)
    
    * NIFI-15910 Improve Connector Secret verification
---
 .../connector/StandardConnectorNode.java           | 175 ++++++++++++++--
 .../connector/TestStandardConnectorNode.java       | 228 ++++++++++++++++++++-
 2 files changed, 376 insertions(+), 27 deletions(-)

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 02675f47299..a647af3b683 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
@@ -78,6 +78,7 @@ import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 public class StandardConnectorNode implements ConnectorNode {
@@ -910,20 +911,9 @@ public class StandardConnectorNode implements 
ConnectorNode {
     @Override
     public List<ConfigVerificationResult> verifyConfigurationStep(final String 
stepName, final StepConfiguration configurationOverrides) {
         logger.debug("Verifying configuration step {} for {}", stepName, this);
-        final List<SecretReference> invalidSecretRefs = new ArrayList<>();
-        final List<AssetReference> invalidAssetRefs = new ArrayList<>();
-        final Map<String, String> resolvedPropertyOverrides = 
resolvePropertyReferences(configurationOverrides, invalidSecretRefs, 
invalidAssetRefs);
-
         final List<ConfigVerificationResult> results = new ArrayList<>();
         try (final NarCloseable ignored = 
NarCloseable.withComponentNarLoader(extensionManager, 
getConnector().getClass(), getIdentifier())) {
 
-            final DescribedValueProvider allowableValueProvider = (step, 
propertyName) -> fetchAllowableValues(step, propertyName, workingFlowContext);
-
-            final MutableConnectorConfigurationContext configContext = 
workingFlowContext.getConfigurationContext().createWithOverrides(stepName, 
resolvedPropertyOverrides);
-            final ConnectorConfiguration connectorConfig = 
configContext.toConnectorConfiguration();
-            final ParameterContextFacade paramContext = 
workingFlowContext.getParameterContext();
-            final ConnectorValidationContext validationContext = new 
StandardConnectorValidationContext(connectorConfig, allowableValueProvider, 
paramContext);
-
             final Optional<ConfigurationStep> optionalStep = 
getConfigurationStep(stepName);
             if (optionalStep.isEmpty()) {
                 results.add(new ConfigVerificationResult.Builder()
@@ -935,6 +925,16 @@ 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);
+
+            final DescribedValueProvider allowableValueProvider = (step, 
propertyName) -> fetchAllowableValues(step, propertyName, workingFlowContext);
+
+            final MutableConnectorConfigurationContext configContext = 
workingFlowContext.getConfigurationContext().createWithOverrides(stepName, 
resolvedPropertyOverrides);
+            final ConnectorConfiguration connectorConfig = 
configContext.toConnectorConfiguration();
+            final ParameterContextFacade paramContext = 
workingFlowContext.getParameterContext();
+            final ConnectorValidationContext validationContext = new 
StandardConnectorValidationContext(connectorConfig, allowableValueProvider, 
paramContext);
 
             final List<ValidationResult> validationResults = new ArrayList<>();
             validatePropertyReferences(configurationStep, 
configurationOverrides, validationResults);
@@ -981,21 +981,30 @@ public class StandardConnectorNode implements 
ConnectorNode {
             .build();
     }
 
-    private Map<String, String> resolvePropertyReferences(final 
StepConfiguration configurationOverrides, final List<SecretReference> 
invalidSecretRefs,
-                                                          final 
List<AssetReference> invalidAssetRefs) {
+    private Map<String, String> resolvePropertyReferences(final 
ConfigurationStep configurationStep, final StepConfiguration 
configurationOverrides,
+                                                          final 
List<SecretReference> invalidSecretRefs, final List<AssetReference> 
invalidAssetRefs) {
 
         final Map<String, String> resolvedProperties = new HashMap<>();
+        final Map<String, ConnectorPropertyDescriptor> descriptorLookup = 
buildPropertyDescriptorLookup(configurationStep);
 
         try {
             // Secret References can be expensive to lookup so we don't want 
to call getSecret() for each one. Instead, we
             // want to find all Secrets by Provider and then call 
fetchSecrets() once per Provider.
-            final Set<SecretReference> secretReferences = 
configurationOverrides.getPropertyValues().values().stream()
-                .filter(Objects::nonNull)
-                .filter(ref -> ref.getValueType() == 
ConnectorValueType.SECRET_REFERENCE)
-                .map(ref -> (SecretReference) ref)
+            // Structurally-empty SECRET_REFERENCE entries (a typed reference 
with no FQN/secretName) are skipped here so the
+            // connector's own required-property validation produces "<name> 
is required" instead of "[null] could not be found".
+            final Set<SecretReference> secretReferences = 
configurationOverrides.getPropertyValues().entrySet().stream()
+                .filter(entry -> entry.getValue() != null && 
entry.getValue().getValueType() == ConnectorValueType.SECRET_REFERENCE)
+                .filter(entry -> !isEmptySecretReference((SecretReference) 
entry.getValue()))
+                .filter(entry -> {
+                    final ConnectorPropertyDescriptor descriptor = 
descriptorLookup.get(entry.getKey());
+                    return descriptor == null || 
isPropertyDependencySatisfied(descriptor, descriptorLookup::get, 
configurationOverrides);
+                })
+                .map(entry -> (SecretReference) entry.getValue())
                 .collect(Collectors.toSet());
 
-            final Map<SecretReference, Secret> secretsByReference = 
initializationContext.getSecretsManager().getSecrets(secretReferences);
+            final Map<SecretReference, Secret> secretsByReference = 
secretReferences.isEmpty()
+                ? Map.of()
+                : 
initializationContext.getSecretsManager().getSecrets(secretReferences);
             secretsByReference.forEach((ref, secret) -> {
                 if (secret == null) {
                     invalidSecretRefs.add(ref);
@@ -1010,9 +1019,24 @@ public class StandardConnectorNode implements 
ConnectorNode {
                     continue;
                 }
 
+                final ConnectorPropertyDescriptor descriptor = 
descriptorLookup.get(propertyName);
+                if (descriptor != null && 
!isPropertyDependencySatisfied(descriptor, descriptorLookup::get, 
configurationOverrides)) {
+                    // Omit values for properties that are not applicable so 
merged configuration does not retain stale overrides
+                    // (createWithOverrides removes keys when the override 
value is null).
+                    resolvedProperties.put(propertyName, null);
+                    continue;
+                }
+
                 // We've already looked up secrets above, so use the cached 
value here.
                 if (valueReference.getValueType() == 
ConnectorValueType.SECRET_REFERENCE) {
                     final SecretReference secretReference = (SecretReference) 
valueReference;
+                    if (isEmptySecretReference(secretReference)) {
+                        // A typed-but-empty SECRET_REFERENCE acts like an 
unset property. The connector's own
+                        // validateConfigurationStep emits "<name> is 
required" for required properties, which is a more
+                        // actionable message than "[null] could not be found" 
from secret resolution.
+                        resolvedProperties.put(propertyName, null);
+                        continue;
+                    }
                     final Secret secret = 
secretsByReference.get(secretReference);
                     final String resolvedValue = (secret == null) ? null : 
secret.getValue();
                     resolvedProperties.put(propertyName, resolvedValue);
@@ -1033,6 +1057,109 @@ public class StandardConnectorNode implements 
ConnectorNode {
         return resolvedProperties;
     }
 
+    private static Map<String, ConnectorPropertyDescriptor> 
buildPropertyDescriptorLookup(final ConfigurationStep configurationStep) {
+        final Map<String, ConnectorPropertyDescriptor> lookup = new 
HashMap<>();
+        for (final ConnectorPropertyGroup propertyGroup : 
configurationStep.getPropertyGroups()) {
+            for (final ConnectorPropertyDescriptor descriptor : 
propertyGroup.getProperties()) {
+                lookup.put(descriptor.getName(), descriptor);
+            }
+        }
+        return lookup;
+    }
+
+    /**
+     * Returns the configured String value of a controlling property for the 
purposes of evaluating a
+     * {@link ConnectorPropertyDependency} against a raw {@link 
StepConfiguration} payload.
+     *
+     * <p>This is an approximation of the value lookup performed inside
+     * {@code AbstractConnector.isDependencySatisfied(...)}, scoped to the 
framework's pre-resolution data model. It
+     * intentionally diverges from the connector-side implementation in two 
cases:
+     * <ul>
+     *   <li>A {@link StringLiteralValue} whose {@link 
StringLiteralValue#getValue() value} is {@code null} falls back to
+     *       {@link ConnectorPropertyDescriptor#getDefaultValue() the 
descriptor default}, whereas the connector-side
+     *       implementation treats a null literal value as no value (and would 
mark the dependency unsatisfied).</li>
+     *   <li>{@link AssetReference} and {@link SecretReference} controlling 
properties are treated as having no
+     *       String value (returns {@code null}, which renders the dependency 
unsatisfied), whereas the connector-side
+     *       implementation works against a {@code ConnectorPropertyValue} 
whose value has already been resolved.</li>
+     * </ul>
+     * Both divergences are acceptable because controlling properties (the 
property a dependency points at) are, in
+     * practice, {@code STRING_LITERAL} values drawn from a fixed set of 
allowable values — typically an enum of strategies
+     * or modes. They are not asset or secret references, and a typed-but-null 
literal can only occur when the controlling
+     * property has been explicitly cleared, in which case treating it as the 
descriptor default matches the user's intent
+     * for the typical "switch back to the default mode" UX.
+     */
+    private static String getConfiguredValueForDependency(final 
StepConfiguration stepConfig, final ConnectorPropertyDescriptor descriptor) {
+        final ConnectorValueReference ref = 
stepConfig.getPropertyValue(descriptor.getName());
+        if (ref == null) {
+            return descriptor.getDefaultValue();
+        }
+        if (ref instanceof StringLiteralValue stringLiteralValue) {
+            final String value = stringLiteralValue.getValue();
+            return value != null ? value : descriptor.getDefaultValue();
+        }
+        return null;
+    }
+
+    private static boolean isPropertyDependencySatisfied(final 
ConnectorPropertyDescriptor propertyDescriptor,
+            final Function<String, ConnectorPropertyDescriptor> 
propertyDescriptorLookup, final StepConfiguration stepConfig) {
+        return isPropertyDependencySatisfied(propertyDescriptor, 
propertyDescriptorLookup, stepConfig, new HashSet<>());
+    }
+
+    /**
+     * A {@link SecretReference} is structurally empty when neither the fully 
qualified name nor the simple secret name is
+     * populated. Such references cannot be resolved against any {@link 
org.apache.nifi.components.connector.secrets.SecretsManager}
+     * and represent an unset property — typically a placeholder emitted by an 
external configuration provider before the user
+     * has filled in the value. Treating these the same as a missing value 
lets connector-level validation surface the more
+     * actionable "is required" message instead of a "[null] could not be 
found" failure.
+     */
+    private static boolean isEmptySecretReference(final SecretReference 
secretReference) {
+        return secretReference.getFullyQualifiedName() == null && 
secretReference.getSecretName() == null;
+    }
+
+    // TODO: consider extracting to a utility class in nifi-api that can be 
shared with AbstractConnector.isDependencySatisfied()
+    private static boolean isPropertyDependencySatisfied(final 
ConnectorPropertyDescriptor propertyDescriptor,
+            final Function<String, ConnectorPropertyDescriptor> 
propertyDescriptorLookup, final StepConfiguration stepConfig, final Set<String> 
propertiesSeen) {
+
+        final Set<ConnectorPropertyDependency> dependencies = 
propertyDescriptor.getDependencies();
+        if (dependencies.isEmpty()) {
+            return true;
+        }
+
+        final boolean added = propertiesSeen.add(propertyDescriptor.getName());
+        if (!added) {
+            return false;
+        }
+
+        try {
+            for (final ConnectorPropertyDependency dependency : dependencies) {
+                final String dependencyName = dependency.getPropertyName();
+
+                final ConnectorPropertyDescriptor dependencyDescriptor = 
propertyDescriptorLookup.apply(dependencyName);
+                if (dependencyDescriptor == null) {
+                    return false;
+                }
+
+                final String dependencyValue = 
getConfiguredValueForDependency(stepConfig, dependencyDescriptor);
+                if (dependencyValue == null) {
+                    return false;
+                }
+
+                if (!isPropertyDependencySatisfied(dependencyDescriptor, 
propertyDescriptorLookup, stepConfig, propertiesSeen)) {
+                    return false;
+                }
+
+                final Set<String> dependentValues = 
dependency.getDependentValues();
+                if (dependentValues != null && 
!dependentValues.contains(dependencyValue)) {
+                    return false;
+                }
+            }
+
+            return true;
+        } finally {
+            propertiesSeen.remove(propertyDescriptor.getName());
+        }
+    }
+
     private String resolvePropertyReference(final ConnectorValueReference 
valueReference) throws IOException {
         if (valueReference == null) {
             return null;
@@ -1506,17 +1633,18 @@ 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(stepConfiguration, invalidSecrets, 
invalidAssets);
+            resolvePropertyReferences(step, stepConfiguration, invalidSecrets, 
invalidAssets);
             addInvalidReferenceResults(allResults, invalidSecrets, 
invalidAssets);
         }
     }
 
     private void addInvalidReferenceResults(final List<ValidationResult> 
results, final List<SecretReference> invalidSecretRefs, final 
List<AssetReference> invalidAssetRefs) {
         for (final SecretReference invalidSecretRef : invalidSecretRefs) {
+            final String secretName = invalidSecretRef.getFullyQualifiedName() 
!= null ? invalidSecretRef.getFullyQualifiedName() : 
invalidSecretRef.getSecretName();
             results.add(new ValidationResult.Builder()
                 .subject("Secret Reference")
                 .valid(false)
-                .explanation("The referenced secret [" + 
invalidSecretRef.getFullyQualifiedName() + "] could not be found")
+                .explanation("The referenced secret [" + secretName + "] could 
not be found")
                 .build());
         }
 
@@ -1530,8 +1658,13 @@ public class StandardConnectorNode implements 
ConnectorNode {
     }
 
     private void validatePropertyReferences(final ConfigurationStep step, 
final StepConfiguration stepConfig, final List<ValidationResult> allResults) {
+        final Map<String, ConnectorPropertyDescriptor> descriptorLookup = 
buildPropertyDescriptorLookup(step);
         for (final ConnectorPropertyGroup propertyGroup : 
step.getPropertyGroups()) {
             for (final ConnectorPropertyDescriptor descriptor : 
propertyGroup.getProperties()) {
+                if (!isPropertyDependencySatisfied(descriptor, 
descriptorLookup::get, stepConfig)) {
+                    continue;
+                }
+
                 final PropertyType propertyType = descriptor.getType();
                 final ConnectorValueReference reference = 
stepConfig.getPropertyValue(descriptor.getName());
 
@@ -1574,7 +1707,7 @@ public class StandardConnectorNode implements 
ConnectorNode {
                 }
             }
             case SecretReference secretReference -> {
-                if (secretReference.getSecretName() == null) {
+                if (isEmptySecretReference(secretReference)) {
                     return true;
                 }
             }
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 c637a22b02f..365c19dbff8 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
@@ -24,6 +24,8 @@ import org.apache.nifi.components.ValidationResult;
 import org.apache.nifi.components.connector.components.FlowContext;
 import org.apache.nifi.components.connector.components.FlowContextType;
 import org.apache.nifi.components.connector.secrets.SecretsManager;
+import org.apache.nifi.components.validation.ValidationState;
+import org.apache.nifi.components.validation.ValidationStatus;
 import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.controller.queue.QueueSize;
 import org.apache.nifi.engine.FlowEngine;
@@ -41,7 +43,9 @@ import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.time.Duration;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -527,6 +531,92 @@ public class TestStandardConnectorNode {
         assertEquals("The property value is invalid", 
failedResult.getExplanation());
     }
 
+    @Test
+    public void 
testVerifyConfigurationStepSkipsSecretReferenceWhenPropertyDependenciesNotMet() 
throws FlowUpdateException {
+        // Use a SecretsManager that fails the test if it is consulted. This 
isolates the dependency-skip path: a regression
+        // 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()))
+            .thenThrow(new AssertionError("SecretsManager.getSecrets must not 
be called when property dependencies are not satisfied"));
+
+        final DependentSecretVerifyConnector connector = new 
DependentSecretVerifyConnector();
+        final StandardConnectorNode connectorNode = 
createConnectorNode(connector, strictSecretsManager);
+
+        connectorNode.transitionStateForUpdating();
+        connectorNode.prepareForUpdate();
+
+        // SecretReference is fully populated so isEmptySecretReference cannot 
carry this test; only the dependency-aware
+        // filter should prevent the SecretsManager lookup.
+        final Map<String, ConnectorValueReference> propertyValues = new 
HashMap<>();
+        propertyValues.put("Mode", new StringLiteralValue("OFF"));
+        propertyValues.put("SecretKey", new SecretReference("pid", "My 
Provider", "my-secret", "My Provider.my-secret"));
+
+        final List<ConfigVerificationResult> results = 
connectorNode.verifyConfigurationStep("authStep", new 
StepConfiguration(propertyValues));
+
+        assertTrue(results.stream().noneMatch(result -> result.getOutcome() == 
ConfigVerificationResult.Outcome.FAILED), results::toString);
+        assertTrue(connector.verifyInvoked);
+    }
+
+    @Test
+    public void 
testVerifyConfigurationStepSurfacesRequiredErrorForEmptySecretReference() 
throws FlowUpdateException {
+        final RequiredSecretConnector connector = new 
RequiredSecretConnector();
+        final StandardConnectorNode connectorNode = 
createConnectorNode(connector);
+
+        connectorNode.transitionStateForUpdating();
+        connectorNode.prepareForUpdate();
+
+        // Structurally-empty SECRET_REFERENCE (only providerName populated) 
for a required, non-dependent secret property.
+        // Without the empty-stub filter this would surface as "[null] could 
not be found"; with it, the connector's
+        // required-property validation should produce "<name> is required" 
instead.
+        final Map<String, ConnectorValueReference> propertyValues = new 
HashMap<>();
+        propertyValues.put("RequiredSecret", new SecretReference(null, "My 
Provider", null, null));
+
+        final List<ConfigVerificationResult> results = 
connectorNode.verifyConfigurationStep("requiredStep", new 
StepConfiguration(propertyValues));
+
+        final List<ConfigVerificationResult> failures = results.stream()
+            .filter(result -> result.getOutcome() == 
ConfigVerificationResult.Outcome.FAILED)
+            .toList();
+        assertFalse(failures.isEmpty(), () -> "Expected a required-property 
failure, got: " + results);
+        assertTrue(failures.stream().noneMatch(result -> {
+            final String explanation = result.getExplanation();
+            return explanation != null && explanation.contains("could not be 
found");
+        }), () -> "Empty SECRET_REFERENCE should not surface as a missing 
secret, got: " + failures);
+        assertTrue(failures.stream().anyMatch(result -> {
+            final String explanation = result.getExplanation();
+            return explanation != null && explanation.contains("RequiredSecret 
is required");
+        }), () -> "Expected required-property failure, got: " + failures);
+    }
+
+    @Test
+    public void 
testPerformValidationSurfacesRequiredErrorForEmptySecretReferenceInActiveContext()
 throws FlowUpdateException {
+        final RequiredSecretConnector connector = new 
RequiredSecretConnector();
+        final StandardConnectorNode connectorNode = 
createConnectorNode(connector);
+
+        // Place a structurally-empty SECRET_REFERENCE into the working 
context, then applyUpdate so it lives in the
+        // active context. performValidation runs against the active context 
and exercises the steady-state validation
+        // code path (validatePropertyReferences -> resolvePropertyReferences 
for each step).
+        connectorNode.transitionStateForUpdating();
+        connectorNode.prepareForUpdate();
+        final Map<String, ConnectorValueReference> propertyValues = new 
HashMap<>();
+        propertyValues.put("RequiredSecret", new SecretReference(null, "My 
Provider", null, null));
+        connectorNode.setConfiguration("requiredStep", new 
StepConfiguration(propertyValues));
+        connectorNode.applyUpdate();
+
+        final ValidationState state = connectorNode.performValidation();
+
+        assertEquals(ValidationStatus.INVALID, state.getStatus(), () -> 
"Expected INVALID validation state, got: " + state);
+        final Collection<ValidationResult> errors = 
state.getValidationErrors();
+        assertTrue(errors.stream().noneMatch(result -> {
+            final String explanation = result.getExplanation();
+            return explanation != null && explanation.contains("could not be 
found");
+        }), () -> "Empty SECRET_REFERENCE should not surface as a missing 
secret in active validation, got: " + errors);
+        assertTrue(errors.stream().anyMatch(result -> {
+            final String explanation = result.getExplanation();
+            return explanation != null && explanation.contains("RequiredSecret 
is required");
+        }), () -> "Expected required-property failure in active validation, 
got: " + errors);
+    }
+
     @Test
     @Timeout(value = 5, unit = TimeUnit.SECONDS)
     public void testDrainFlowFilesTransitionsStateToDraining() throws 
FlowUpdateException {
@@ -639,6 +729,13 @@ public class TestStandardConnectorNode {
     }
 
     private StandardConnectorNode createConnectorNode(final Connector 
connector) throws FlowUpdateException {
+        final SecretsManager defaultSecretsManager = 
mock(SecretsManager.class);
+        when(defaultSecretsManager.getAllSecrets()).thenReturn(List.of());
+        
when(defaultSecretsManager.getSecrets(anySet())).thenReturn(Collections.emptyMap());
+        return createConnectorNode(connector, defaultSecretsManager);
+    }
+
+    private StandardConnectorNode createConnectorNode(final Connector 
connector, final SecretsManager initializedSecretsManager) throws 
FlowUpdateException {
         final ConnectorStateTransition stateTransition = new 
StandardConnectorStateTransition("TestConnectorNode");
         final ConnectorValidationTrigger validationTrigger = new 
SynchronousConnectorValidationTrigger();
         final StandardConnectorNode node = new StandardConnectorNode(
@@ -655,13 +752,8 @@ public class TestStandardConnectorNode {
             validationTrigger,
             false);
 
-        // mock secrets manager
-        final SecretsManager secretsManager = mock(SecretsManager.class);
-        when(secretsManager.getAllSecrets()).thenReturn(List.of());
-        
when(secretsManager.getSecrets(anySet())).thenReturn(Collections.emptyMap());
-
         final FrameworkConnectorInitializationContext initializationContext = 
mock(FrameworkConnectorInitializationContext.class);
-        
when(initializationContext.getSecretsManager()).thenReturn(secretsManager);
+        
when(initializationContext.getSecretsManager()).thenReturn(initializedSecretsManager);
 
         node.initializeConnector(initializationContext);
         node.loadInitialFlow();
@@ -833,6 +925,130 @@ public class TestStandardConnectorNode {
         }
     }
 
+    /**
+     * Connector whose secret property is gated by another property; used to 
ensure verify does not resolve or validate
+     * secret references when dependencies are not satisfied (empty getSecrets 
map would otherwise fail verification).
+     */
+    private static class DependentSecretVerifyConnector extends 
AbstractConnector {
+        private boolean verifyInvoked;
+
+        @Override
+        public VersionedExternalFlow getInitialFlow() {
+            return null;
+        }
+
+        @Override
+        public void prepareForUpdate(final FlowContext workingContext, final 
FlowContext activeContext) {
+        }
+
+        @Override
+        public List<ConfigurationStep> getConfigurationSteps() {
+            final ConnectorPropertyDescriptor modeProperty = new 
ConnectorPropertyDescriptor.Builder()
+                .name("Mode")
+                .description("Authentication mode")
+                .required(true)
+                .defaultValue("OFF")
+                .allowableValues("OFF", "WITH_SECRET")
+                .build();
+
+            final ConnectorPropertyDescriptor secretProperty = new 
ConnectorPropertyDescriptor.Builder()
+                .name("SecretKey")
+                .description("Secret when mode requires it")
+                .type(PropertyType.SECRET)
+                .dependsOn(modeProperty, "WITH_SECRET")
+                .build();
+
+            final ConnectorPropertyGroup propertyGroup = 
ConnectorPropertyGroup.builder()
+                .name("g")
+                .description("g")
+                .properties(List.of(modeProperty, secretProperty))
+                .build();
+
+            final ConfigurationStep authStep = new ConfigurationStep.Builder()
+                .name("authStep")
+                .propertyGroups(List.of(propertyGroup))
+                .build();
+
+            return List.of(authStep);
+        }
+
+        @Override
+        public List<ValidationResult> validateConfigurationStep(final 
ConfigurationStep configurationStep, final ConnectorConfigurationContext 
connectorConfigurationContext,
+                final ConnectorValidationContext connectorValidationContext) {
+            return List.of();
+        }
+
+        @Override
+        public void applyUpdate(final FlowContext workingContext, final 
FlowContext activeContext) {
+        }
+
+        @Override
+        protected void onStepConfigured(final String stepName, final 
FlowContext workingContext) {
+        }
+
+        @Override
+        public List<ConfigVerificationResult> verifyConfigurationStep(final 
String stepName, final Map<String, String> overrides, final FlowContext 
flowContext) {
+            verifyInvoked = true;
+            return List.of(new ConfigVerificationResult.Builder()
+                .verificationStepName("Custom verify")
+                .outcome(ConfigVerificationResult.Outcome.SUCCESSFUL)
+                .build());
+        }
+    }
+
+    /**
+     * Connector with a required, non-dependent SECRET property. Used to 
confirm that when the framework receives a
+     * structurally-empty SECRET_REFERENCE, it surfaces the connector's "is 
required" message rather than a
+     * "[null] could not be found" failure from secret resolution. Defers to 
AbstractConnector.validateConfigurationStep
+     * so the required-property check actually runs.
+     */
+    private static class RequiredSecretConnector extends AbstractConnector {
+        @Override
+        public VersionedExternalFlow getInitialFlow() {
+            return null;
+        }
+
+        @Override
+        public void prepareForUpdate(final FlowContext workingContext, final 
FlowContext activeContext) {
+        }
+
+        @Override
+        public List<ConfigurationStep> getConfigurationSteps() {
+            final ConnectorPropertyDescriptor secretProperty = new 
ConnectorPropertyDescriptor.Builder()
+                .name("RequiredSecret")
+                .description("A required secret with no dependencies")
+                .type(PropertyType.SECRET)
+                .required(true)
+                .build();
+
+            final ConnectorPropertyGroup propertyGroup = 
ConnectorPropertyGroup.builder()
+                .name("g")
+                .description("g")
+                .properties(List.of(secretProperty))
+                .build();
+
+            final ConfigurationStep step = new ConfigurationStep.Builder()
+                .name("requiredStep")
+                .propertyGroups(List.of(propertyGroup))
+                .build();
+
+            return List.of(step);
+        }
+
+        @Override
+        public void applyUpdate(final FlowContext workingContext, final 
FlowContext activeContext) {
+        }
+
+        @Override
+        protected void onStepConfigured(final String stepName, final 
FlowContext workingContext) {
+        }
+
+        @Override
+        public List<ConfigVerificationResult> verifyConfigurationStep(final 
String stepName, final Map<String, String> overrides, final FlowContext 
flowContext) {
+            return List.of();
+        }
+    }
+
     /**
      * Test connector that allows control over when drainFlowFiles completes 
via a CompletableFuture
      */

Reply via email to