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
*/