Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public class NiFiProperties extends ApplicationProperties {
// Secrets Manager properties
public static final String SECRETS_MANAGER_IMPLEMENTATION = "nifi.secrets.manager.implementation";
public static final String SECRETS_MANAGER_CACHE_DURATION = "nifi.secrets.manager.cache.duration";
public static final String SECRETS_MANAGER_UNRESTRICTED_TAG_NAME = "nifi.secrets.manager.unrestricted.tag.name";

// security properties
public static final String SECURITY_KEYSTORE = "nifi.security.keystore";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class SecretDTO {
private String name;
private String fullyQualifiedName;
private String description;
private String propertyProtectionType;

@Schema(description = "The identifier of the secret provider that manages this secret.")
public String getProviderId() {
Expand Down Expand Up @@ -85,5 +86,14 @@ public String getDescription() {
public void setDescription(final String description) {
this.description = description;
}

@Schema(description = "Whether the secret may be referenced only by sensitive properties (RESTRICTED) or by any property (UNRESTRICTED).")
public String getPropertyProtectionType() {
return propertyProtectionType;
}

public void setPropertyProtectionType(final String propertyProtectionType) {
this.propertyProtectionType = propertyProtectionType;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.nifi.components.connector.secrets;

import org.apache.nifi.components.connector.PropertyProtectionType;
import org.apache.nifi.components.connector.Secret;
import org.apache.nifi.controller.ParameterProviderNode;
import org.apache.nifi.parameter.Parameter;
Expand All @@ -28,9 +29,15 @@

public class ParameterProviderSecretProvider implements SecretProvider {
private final ParameterProviderNode parameterProvider;
private final String unrestrictedTagName;

public ParameterProviderSecretProvider(final ParameterProviderNode parameterProvider) {
this(parameterProvider, null);
}

public ParameterProviderSecretProvider(final ParameterProviderNode parameterProvider, final String unrestrictedTagName) {
this.parameterProvider = parameterProvider;
this.unrestrictedTagName = unrestrictedTagName;
}

@Override
Expand Down Expand Up @@ -61,6 +68,19 @@ public List<Secret> getAllSecrets() {
private Secret createSecret(final String groupName, final Parameter parameter) {
final ParameterDescriptor descriptor = parameter.getDescriptor();

// A Secret is UNRESTRICTED (usable by non-sensitive properties) only when the deployment has
// configured an unrestricted tag name and the backing Parameter carries that tag with a value
// of "true" (case-insensitive). Otherwise the Secret remains RESTRICTED. The value is read via
// the constant-first idiom so a null tag value does not throw.
PropertyProtectionType protectionType = PropertyProtectionType.RESTRICTED;
if (unrestrictedTagName != null && !unrestrictedTagName.isBlank()) {
final boolean unrestricted = parameter.getTags().stream()
.anyMatch(tag -> unrestrictedTagName.equals(tag.getKey()) && "true".equalsIgnoreCase(tag.getValue()));
if (unrestricted) {
protectionType = PropertyProtectionType.UNRESTRICTED;
}
}

return new StandardSecret.Builder()
.providerId(getProviderId())
.providerName(getProviderName())
Expand All @@ -70,6 +90,7 @@ private Secret createSecret(final String groupName, final Parameter parameter) {
.description(descriptor.getDescription())
.value(parameter.getValue())
.authorizable(parameterProvider)
.propertyProtectionType(protectionType)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class ParameterProviderSecretsManager implements SecretsManager {

private FlowManager flowManager;
private Duration cacheDuration;
private String unrestrictedTagName;
private final Map<String, CachedSecret> secretCache = new ConcurrentHashMap<>();

// Per-ParameterProvider-id deduplication for the WARN log emitted whenever a SecretReference
Expand All @@ -69,6 +70,8 @@ public void initialize(final SecretsManagerInitializationContext initializationC
final String cacheDurationValue = initializationContext.getApplicationProperty(NiFiProperties.SECRETS_MANAGER_CACHE_DURATION);
final String effectiveDuration = cacheDurationValue == null ? DEFAULT_CACHE_DURATION : cacheDurationValue;
this.cacheDuration = Duration.ofNanos(FormatUtils.getTimeDuration(effectiveDuration.trim(), TimeUnit.NANOSECONDS));

this.unrestrictedTagName = initializationContext.getApplicationProperty(NiFiProperties.SECRETS_MANAGER_UNRESTRICTED_TAG_NAME);
}

@Override
Expand Down Expand Up @@ -122,7 +125,7 @@ public Set<SecretProvider> getSecretProviders() {
logger.info("{} returned to VALID after being logged as [{}] now resolving Secret References", parameterProvider, priorWarnedStatus);
}
}
providers.add(new ParameterProviderSecretProvider(parameterProviderNode));
providers.add(new ParameterProviderSecretProvider(parameterProviderNode, unrestrictedTagName));
}

return providers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.apache.nifi.authorization.Resource;
import org.apache.nifi.authorization.resource.Authorizable;
import org.apache.nifi.components.connector.PropertyProtectionType;

import java.util.Objects;

Expand All @@ -31,6 +32,7 @@ public class StandardSecret implements AuthorizableSecret {
private final String description;
private final String value;
private final Authorizable authorizable;
private final PropertyProtectionType propertyProtectionType;

private StandardSecret(final Builder builder) {
this.providerId = builder.providerId;
Expand All @@ -40,6 +42,7 @@ private StandardSecret(final Builder builder) {
this.description = builder.description;
this.value = builder.value;
this.authorizable = builder.authorizable;
this.propertyProtectionType = builder.propertyProtectionType;
this.fullyQualifiedName = builder.fullyQualifiedName == null ? groupName + "." + name : builder.fullyQualifiedName;
}

Expand Down Expand Up @@ -78,6 +81,11 @@ public String getFullyQualifiedName() {
return fullyQualifiedName;
}

@Override
public PropertyProtectionType getPropertyProtectionType() {
return propertyProtectionType;
}

@Override
public String toString() {
return "StandardSecret[providerName=%s, groupName=%s, name=%s]".formatted(providerName, groupName, name);
Expand Down Expand Up @@ -121,6 +129,7 @@ public static class Builder {
private String description;
private String value;
private Authorizable authorizable;
private PropertyProtectionType propertyProtectionType = PropertyProtectionType.RESTRICTED;

public Builder providerId(final String providerId) {
this.providerId = providerId;
Expand Down Expand Up @@ -162,6 +171,11 @@ public Builder fullyQualifiedName(final String fullyQualifiedName) {
return this;
}

public Builder propertyProtectionType(final PropertyProtectionType propertyProtectionType) {
this.propertyProtectionType = propertyProtectionType == null ? PropertyProtectionType.RESTRICTED : propertyProtectionType;
return this;
}

public StandardSecret build() {
if (groupName == null) {
throw new IllegalStateException("Group name is required");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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.components.connector.secrets;

import org.apache.nifi.components.connector.PropertyProtectionType;
import org.apache.nifi.components.connector.Secret;
import org.apache.nifi.controller.ParameterProviderNode;
import org.apache.nifi.parameter.Parameter;
import org.apache.nifi.parameter.ParameterDescriptor;
import org.apache.nifi.parameter.ParameterGroup;
import org.apache.nifi.parameter.ParameterTag;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TestParameterProviderSecretProvider {

private static final String TAG_NAME = "nifi.unrestricted";

@Test
public void testTagValueTrueLowercaseIsUnrestricted() {
assertEquals(PropertyProtectionType.UNRESTRICTED, classify(TAG_NAME, List.of(new ParameterTag(TAG_NAME, "true"))));
}

@Test
public void testTagValueTrueTitlecaseIsUnrestricted() {
assertEquals(PropertyProtectionType.UNRESTRICTED, classify(TAG_NAME, List.of(new ParameterTag(TAG_NAME, "True"))));
}

@Test
public void testTagValueTrueUppercaseIsUnrestricted() {
assertEquals(PropertyProtectionType.UNRESTRICTED, classify(TAG_NAME, List.of(new ParameterTag(TAG_NAME, "TRUE"))));
}

@Test
public void testTagValueFalseIsRestricted() {
assertEquals(PropertyProtectionType.RESTRICTED, classify(TAG_NAME, List.of(new ParameterTag(TAG_NAME, "false"))));
}

@Test
public void testTagValueNullIsRestricted() {
assertEquals(PropertyProtectionType.RESTRICTED, classify(TAG_NAME, List.of(new ParameterTag(TAG_NAME, null))));
}

@Test
public void testTagKeyMismatchIsRestricted() {
assertEquals(PropertyProtectionType.RESTRICTED, classify(TAG_NAME, List.of(new ParameterTag("other-tag", "true"))));
}

@Test
public void testNoTagsIsRestricted() {
assertEquals(PropertyProtectionType.RESTRICTED, classify(TAG_NAME, List.of()));
}

@Test
public void testConfiguredNameNullIsAlwaysRestricted() {
assertEquals(PropertyProtectionType.RESTRICTED, classify(null, List.of(new ParameterTag(TAG_NAME, "true"))));
}

@Test
public void testConfiguredNameBlankIsAlwaysRestricted() {
assertEquals(PropertyProtectionType.RESTRICTED, classify(" ", List.of(new ParameterTag(TAG_NAME, "true"))));
}

@Test
public void testMultipleTagsOneMatchIsUnrestricted() {
assertEquals(PropertyProtectionType.UNRESTRICTED, classify(TAG_NAME,
List.of(new ParameterTag("other-tag", "false"), new ParameterTag(TAG_NAME, "true"))));
}

private PropertyProtectionType classify(final String configuredTagName, final List<ParameterTag> tags) {
final ParameterProviderNode node = mock(ParameterProviderNode.class);
when(node.getIdentifier()).thenReturn("provider-id");
when(node.getName()).thenReturn("Provider");

final ParameterDescriptor descriptor = new ParameterDescriptor.Builder()
.name("secret")
.description("description")
.build();
final Parameter parameter = new Parameter.Builder()
.descriptor(descriptor)
.value("secret-value")
.tags(tags)
.build();
when(node.fetchParameterValues()).thenReturn(List.of(new ParameterGroup("Group", List.of(parameter))));

final ParameterProviderSecretProvider provider = new ParameterProviderSecretProvider(node, configuredTagName);
final List<Secret> secrets = provider.getAllSecrets();
assertEquals(1, secrets.size());
return secrets.get(0).getPropertyProtectionType();
}
}
Loading
Loading