NIFI-14025 Corrected LDAP Provider Trust Store Configuration

- Fixed LDAP Provider support for configuring a Trust Store without a Key Store

Signed-off-by: Pierre Villard <pierre.villard.fr@gmail.com>

This closes #9544.
This commit is contained in:
exceptionfactory 2024-11-19 13:30:42 -06:00 committed by Pierre Villard
parent e327dcdd21
commit a3f4f7b964
No known key found for this signature in database
GPG Key ID: F92A93B30C07C6D5
6 changed files with 354 additions and 190 deletions

View File

@ -91,6 +91,12 @@
<version>7.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-cert-builder</artifactId>
<version>2.1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
<name>nifi-ldap-iaa-providers</name>
</project>

View File

@ -16,12 +16,6 @@
*/
package org.apache.nifi.ldap;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -38,8 +32,8 @@ import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException
import org.apache.nifi.authentication.exception.ProviderCreationException;
import org.apache.nifi.authentication.exception.ProviderDestructionException;
import org.apache.nifi.configuration.NonComponentConfigurationContext;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardSslContextBuilder;
import org.apache.nifi.ldap.ssl.LdapSslContextProvider;
import org.apache.nifi.ldap.ssl.StandardLdapSslContextProvider;
import org.apache.nifi.util.FormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -249,92 +243,9 @@ public class LdapProvider implements LoginIdentityProvider {
}
private static SSLContext getConfiguredSslContext(final NonComponentConfigurationContext configurationContext) {
final String rawProtocol = configurationContext.getProperty(ProviderProperty.TLS_PROTOCOL.getProperty());
SSLContext sslContext = null;
try {
final KeyStore trustStore = getTrustStore(configurationContext);
if (trustStore == null) {
logger.debug("Truststore not configured");
} else {
final StandardSslContextBuilder sslContextBuilder = new StandardSslContextBuilder();
sslContextBuilder.protocol(rawProtocol);
sslContextBuilder.trustStore(trustStore);
final KeyStore keyStore = getKeyStore(configurationContext);
if (keyStore == null) {
logger.debug("Keystore not configured");
} else {
final String keyStorePassword = configurationContext.getProperty(ProviderProperty.KEYSTORE_PASSWORD.getProperty());
final char[] keyPassword = keyStorePassword.toCharArray();
sslContextBuilder.keyStore(keyStore);
sslContextBuilder.keyPassword(keyPassword);
sslContext = sslContextBuilder.build();
}
}
} catch (final Exception e) {
logger.error("Encountered an error configuring TLS for LDAP identity provider: {}", e.getLocalizedMessage());
throw new ProviderCreationException("Error configuring TLS for LDAP identity provider", e);
}
return sslContext;
}
private static KeyStore getKeyStore(final NonComponentConfigurationContext configurationContext) throws IOException {
final String rawKeystore = configurationContext.getProperty(ProviderProperty.KEYSTORE.getProperty());
final String rawKeystorePassword = configurationContext.getProperty(ProviderProperty.KEYSTORE_PASSWORD.getProperty());
final String rawKeystoreType = configurationContext.getProperty(ProviderProperty.KEYSTORE_TYPE.getProperty());
final KeyStore keyStore;
if (rawKeystore == null || rawKeystore.isBlank()) {
keyStore = null;
} else if (rawKeystorePassword == null) {
throw new ProviderCreationException("Keystore Password not configured");
} else {
final StandardKeyStoreBuilder builder = new StandardKeyStoreBuilder();
builder.type(rawKeystoreType);
final char[] keyStorePassword = rawKeystorePassword.toCharArray();
builder.password(keyStorePassword);
final Path trustStorePath = Paths.get(rawKeystore);
try (InputStream trustStoreStream = Files.newInputStream(trustStorePath)) {
builder.inputStream(trustStoreStream);
keyStore = builder.build();
}
}
return keyStore;
}
private static KeyStore getTrustStore(final NonComponentConfigurationContext configurationContext) throws IOException {
final String rawTruststore = configurationContext.getProperty(ProviderProperty.TRUSTSTORE.getProperty());
final String rawTruststorePassword = configurationContext.getProperty(ProviderProperty.TRUSTSTORE_PASSWORD.getProperty());
final String rawTruststoreType = configurationContext.getProperty(ProviderProperty.TRUSTSTORE_TYPE.getProperty());
final KeyStore trustStore;
if (rawTruststore == null || rawTruststore.isBlank()) {
trustStore = null;
} else if (rawTruststorePassword == null) {
throw new ProviderCreationException("Truststore Password not configured");
} else {
final StandardKeyStoreBuilder builder = new StandardKeyStoreBuilder();
builder.type(rawTruststoreType);
final char[] trustStorePassword = rawTruststorePassword.toCharArray();
builder.password(trustStorePassword);
final Path trustStorePath = Paths.get(rawTruststore);
try (InputStream trustStoreStream = Files.newInputStream(trustStorePath)) {
builder.inputStream(trustStoreStream);
trustStore = builder.build();
}
}
return trustStore;
final LdapSslContextProvider ldapSslContextProvider = new StandardLdapSslContextProvider();
final Map<String, String> contextProperties = configurationContext.getProperties();
return ldapSslContextProvider.createContext(contextProperties);
}
@Override

View File

@ -0,0 +1,33 @@
/*
* 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.ldap.ssl;
import javax.net.ssl.SSLContext;
import java.util.Map;
/**
* Abstraction for creating an SSLContext from LDAP configuration properties
*/
public interface LdapSslContextProvider {
/**
* Create SSLContext from configuration properties
*
* @param properties Provider properties
* @return SSLContext
*/
SSLContext createContext(Map<String, String> properties);
}

View File

@ -0,0 +1,147 @@
/*
* 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.ldap.ssl;
import org.apache.nifi.authentication.exception.ProviderCreationException;
import org.apache.nifi.ldap.ProviderProperty;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardSslContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.util.Map;
import java.util.Objects;
/**
* Standard implementation of LDAP SSLContext Provider supporting common properties
*/
public class StandardLdapSslContextProvider implements LdapSslContextProvider {
private static final Logger logger = LoggerFactory.getLogger(StandardLdapSslContextProvider.class);
private static final String DEFAULT_PROTOCOL = "TLS";
/**
* Create SSLContext using configured properties defaulting to system trust store when trust store properties not configured
*
* @param properties Provider properties
* @return SSLContext initialized using configured properties
*/
@Override
public SSLContext createContext(final Map<String, String> properties) {
Objects.requireNonNull(properties, "Properties required");
final String rawProtocol = properties.get(ProviderProperty.TLS_PROTOCOL.getProperty());
final String protocol;
if (rawProtocol == null || rawProtocol.isBlank()) {
protocol = DEFAULT_PROTOCOL;
} else {
protocol = rawProtocol;
}
try {
final SSLContext sslContext;
final StandardSslContextBuilder sslContextBuilder = new StandardSslContextBuilder();
sslContextBuilder.protocol(protocol);
final KeyStore trustStore = getTrustStore(properties);
if (trustStore == null) {
logger.debug("LDAP TLS Truststore not configured");
} else {
sslContextBuilder.trustStore(trustStore);
}
final KeyStore keyStore = getKeyStore(properties);
if (keyStore == null) {
logger.debug("LDAP TLS Keystore not configured");
} else {
final String keyStorePassword = properties.get(ProviderProperty.KEYSTORE_PASSWORD.getProperty());
final char[] keyPassword = keyStorePassword.toCharArray();
sslContextBuilder.keyStore(keyStore);
sslContextBuilder.keyPassword(keyPassword);
}
sslContext = sslContextBuilder.build();
return sslContext;
} catch (final Exception e) {
throw new ProviderCreationException("Error configuring TLS for LDAP Provider", e);
}
}
private KeyStore getKeyStore(final Map<String, String> properties) throws IOException {
final String rawKeystore = properties.get(ProviderProperty.KEYSTORE.getProperty());
final String rawKeystorePassword = properties.get(ProviderProperty.KEYSTORE_PASSWORD.getProperty());
final String rawKeystoreType = properties.get(ProviderProperty.KEYSTORE_TYPE.getProperty());
final KeyStore keyStore;
if (rawKeystore == null || rawKeystore.isBlank()) {
keyStore = null;
} else if (rawKeystorePassword == null) {
throw new ProviderCreationException("Keystore Password not configured");
} else {
final StandardKeyStoreBuilder builder = new StandardKeyStoreBuilder();
builder.type(rawKeystoreType);
final char[] keyStorePassword = rawKeystorePassword.toCharArray();
builder.password(keyStorePassword);
final Path trustStorePath = Paths.get(rawKeystore);
try (InputStream trustStoreStream = Files.newInputStream(trustStorePath)) {
builder.inputStream(trustStoreStream);
keyStore = builder.build();
}
}
return keyStore;
}
private KeyStore getTrustStore(final Map<String, String> properties) throws IOException {
final String rawTruststore = properties.get(ProviderProperty.TRUSTSTORE.getProperty());
final String rawTruststorePassword = properties.get(ProviderProperty.TRUSTSTORE_PASSWORD.getProperty());
final String rawTruststoreType = properties.get(ProviderProperty.TRUSTSTORE_TYPE.getProperty());
final KeyStore trustStore;
if (rawTruststore == null || rawTruststore.isBlank()) {
trustStore = null;
} else if (rawTruststorePassword == null) {
throw new ProviderCreationException("Truststore Password not configured");
} else {
final StandardKeyStoreBuilder builder = new StandardKeyStoreBuilder();
builder.type(rawTruststoreType);
final char[] trustStorePassword = rawTruststorePassword.toCharArray();
builder.password(trustStorePassword);
final Path trustStorePath = Paths.get(rawTruststore);
try (InputStream trustStoreStream = Files.newInputStream(trustStorePath)) {
builder.inputStream(trustStoreStream);
trustStore = builder.build();
}
}
return trustStore;
}
}

View File

@ -18,7 +18,6 @@ package org.apache.nifi.ldap.tenants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.nifi.authentication.exception.ProviderCreationException;
import org.apache.nifi.authentication.exception.ProviderDestructionException;
import org.apache.nifi.authorization.AuthorizerConfigurationContext;
import org.apache.nifi.authorization.Group;
@ -34,10 +33,9 @@ import org.apache.nifi.authorization.util.IdentityMappingUtil;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.ldap.LdapAuthenticationStrategy;
import org.apache.nifi.ldap.LdapsSocketFactory;
import org.apache.nifi.ldap.ProviderProperty;
import org.apache.nifi.ldap.ReferralStrategy;
import org.apache.nifi.security.ssl.StandardKeyStoreBuilder;
import org.apache.nifi.security.ssl.StandardSslContextBuilder;
import org.apache.nifi.ldap.ssl.LdapSslContextProvider;
import org.apache.nifi.ldap.ssl.StandardLdapSslContextProvider;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger;
@ -65,12 +63,6 @@ import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.SearchControls;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -835,91 +827,8 @@ public class LdapUserGroupProvider implements UserGroupProvider {
}
private SSLContext getConfiguredSslContext(final AuthorizerConfigurationContext configurationContext) {
final String rawProtocol = configurationContext.getProperty(ProviderProperty.TLS_PROTOCOL.getProperty()).getValue();
SSLContext sslContext = null;
try {
final KeyStore trustStore = getTrustStore(configurationContext);
if (trustStore == null) {
logger.debug("Truststore not configured");
} else {
final StandardSslContextBuilder sslContextBuilder = new StandardSslContextBuilder();
sslContextBuilder.protocol(rawProtocol);
sslContextBuilder.trustStore(trustStore);
final KeyStore keyStore = getKeyStore(configurationContext);
if (keyStore == null) {
logger.debug("Keystore not configured");
} else {
final String keyStorePassword = configurationContext.getProperty(ProviderProperty.KEYSTORE_PASSWORD.getProperty()).getValue();
final char[] keyPassword = keyStorePassword.toCharArray();
sslContextBuilder.keyStore(keyStore);
sslContextBuilder.keyPassword(keyPassword);
sslContext = sslContextBuilder.build();
}
}
} catch (final Exception e) {
logger.error("Encountered an error configuring TLS for LDAP user group provider: {}", e.getLocalizedMessage());
throw new ProviderCreationException("Error configuring TLS for LDAP user group provider", e);
}
return sslContext;
}
private static KeyStore getKeyStore(final AuthorizerConfigurationContext configurationContext) throws IOException {
final String rawKeystore = configurationContext.getProperty(ProviderProperty.KEYSTORE.getProperty()).getValue();
final String rawKeystorePassword = configurationContext.getProperty(ProviderProperty.KEYSTORE_PASSWORD.getProperty()).getValue();
final String rawKeystoreType = configurationContext.getProperty(ProviderProperty.KEYSTORE_TYPE.getProperty()).getValue();
final KeyStore keyStore;
if (rawKeystore == null || rawKeystore.isBlank()) {
keyStore = null;
} else if (rawKeystorePassword == null) {
throw new ProviderCreationException("Keystore Password not configured");
} else {
final StandardKeyStoreBuilder builder = new StandardKeyStoreBuilder();
builder.type(rawKeystoreType);
final char[] keyStorePassword = rawKeystorePassword.toCharArray();
builder.password(keyStorePassword);
final Path trustStorePath = Paths.get(rawKeystore);
try (InputStream trustStoreStream = Files.newInputStream(trustStorePath)) {
builder.inputStream(trustStoreStream);
keyStore = builder.build();
}
}
return keyStore;
}
private static KeyStore getTrustStore(final AuthorizerConfigurationContext configurationContext) throws IOException {
final String rawTruststore = configurationContext.getProperty(ProviderProperty.TRUSTSTORE.getProperty()).getValue();
final String rawTruststorePassword = configurationContext.getProperty(ProviderProperty.TRUSTSTORE_PASSWORD.getProperty()).getValue();
final String rawTruststoreType = configurationContext.getProperty(ProviderProperty.TRUSTSTORE_TYPE.getProperty()).getValue();
final KeyStore trustStore;
if (rawTruststore == null || rawTruststore.isBlank()) {
trustStore = null;
} else if (rawTruststorePassword == null) {
throw new ProviderCreationException("Truststore Password not configured");
} else {
final StandardKeyStoreBuilder builder = new StandardKeyStoreBuilder();
builder.type(rawTruststoreType);
final char[] trustStorePassword = rawTruststorePassword.toCharArray();
builder.password(trustStorePassword);
final Path trustStorePath = Paths.get(rawTruststore);
try (InputStream trustStoreStream = Files.newInputStream(trustStorePath)) {
builder.inputStream(trustStoreStream);
trustStore = builder.build();
}
}
return trustStore;
final LdapSslContextProvider ldapSslContextProvider = new StandardLdapSslContextProvider();
final Map<String, String> contextProperties = configurationContext.getProperties();
return ldapSslContextProvider.createContext(contextProperties);
}
}

View File

@ -0,0 +1,158 @@
/*
* 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.ldap.ssl;
import org.apache.nifi.ldap.ProviderProperty;
import org.apache.nifi.security.cert.builder.StandardCertificateBuilder;
import org.apache.nifi.security.ssl.EphemeralKeyStoreBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import javax.net.ssl.SSLContext;
import javax.security.auth.x500.X500Principal;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class StandardLdapSslContextProviderTest {
private static final String TLS_PROTOCOL = "TLS";
private static final String ALIAS = "entry-0";
private static final String KEY_STORE_EXTENSION = ".p12";
private static final String KEY_STORE_PASS = UUID.randomUUID().toString();
private static final String TRUST_STORE_PASS = UUID.randomUUID().toString();
@TempDir
private static Path keyStoreDirectory;
private static String keyStoreType;
private static Path keyStorePath;
private static String trustStoreType;
private static Path trustStorePath;
private StandardLdapSslContextProvider provider;
@BeforeAll
public static void setConfiguration() throws Exception {
final KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
final X509Certificate certificate = new StandardCertificateBuilder(keyPair, new X500Principal("CN=localhost"), Duration.ofHours(1)).build();
final KeyStore keyStore = new EphemeralKeyStoreBuilder().build();
keyStore.setKeyEntry(ALIAS, keyPair.getPrivate(), KEY_STORE_PASS.toCharArray(), new Certificate[]{certificate});
keyStorePath = Files.createTempFile(keyStoreDirectory, "keyStore", KEY_STORE_EXTENSION);
try (OutputStream outputStream = Files.newOutputStream(keyStorePath)) {
keyStore.store(outputStream, KEY_STORE_PASS.toCharArray());
}
keyStoreType = keyStore.getType().toUpperCase();
final KeyStore trustStore = new EphemeralKeyStoreBuilder().addCertificate(certificate).build();
trustStorePath = Files.createTempFile(keyStoreDirectory, "trustStore", KEY_STORE_EXTENSION);
try (OutputStream outputStream = Files.newOutputStream(trustStorePath)) {
trustStore.store(outputStream, TRUST_STORE_PASS.toCharArray());
}
trustStoreType = trustStore.getType().toUpperCase();
}
@BeforeEach
void setProvider() {
provider = new StandardLdapSslContextProvider();
}
@Test
void testCreateContextEmptyProperties() {
final SSLContext sslContext = provider.createContext(Map.of());
assertNotNull(sslContext);
}
@Test
void testCreateContextProtocol() {
final Map<String, String> properties = Map.of(
ProviderProperty.TLS_PROTOCOL.getProperty(), TLS_PROTOCOL
);
final SSLContext sslContext = provider.createContext(properties);
assertNotNull(sslContext);
}
@Test
void testCreateContextTrustStore() {
final Map<String, String> properties = Map.of(
ProviderProperty.TLS_PROTOCOL.getProperty(), TLS_PROTOCOL,
ProviderProperty.TRUSTSTORE.getProperty(), trustStorePath.toString(),
ProviderProperty.TRUSTSTORE_TYPE.getProperty(), trustStoreType,
ProviderProperty.TRUSTSTORE_PASSWORD.getProperty(), TRUST_STORE_PASS
);
final SSLContext sslContext = provider.createContext(properties);
assertNotNull(sslContext);
}
@Test
void testCreateContextKeyStore() {
final Map<String, String> properties = Map.of(
ProviderProperty.TLS_PROTOCOL.getProperty(), TLS_PROTOCOL,
ProviderProperty.KEYSTORE.getProperty(), keyStorePath.toString(),
ProviderProperty.KEYSTORE_TYPE.getProperty(), keyStoreType,
ProviderProperty.KEYSTORE_PASSWORD.getProperty(), KEY_STORE_PASS
);
final SSLContext sslContext = provider.createContext(properties);
assertNotNull(sslContext);
}
@Test
void testCreateContext() {
final Map<String, String> properties = Map.of(
ProviderProperty.TLS_PROTOCOL.getProperty(), TLS_PROTOCOL,
ProviderProperty.TRUSTSTORE.getProperty(), trustStorePath.toString(),
ProviderProperty.TRUSTSTORE_TYPE.getProperty(), trustStoreType,
ProviderProperty.TRUSTSTORE_PASSWORD.getProperty(), TRUST_STORE_PASS,
ProviderProperty.KEYSTORE.getProperty(), keyStorePath.toString(),
ProviderProperty.KEYSTORE_TYPE.getProperty(), keyStoreType,
ProviderProperty.KEYSTORE_PASSWORD.getProperty(), KEY_STORE_PASS
);
final SSLContext sslContext = provider.createContext(properties);
assertNotNull(sslContext);
}
}