Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BE: Auth: Support LDAPS for AD #840

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
13 changes: 13 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@
<version>${okhttp3.mockwebserver.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${confluent.version}-ccs</version>
<classifier>test</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.80</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import io.kafbat.ui.service.rbac.AccessControlService;
import io.kafbat.ui.service.rbac.extractor.RbacActiveDirectoryAuthoritiesExtractor;
import io.kafbat.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
import io.kafbat.ui.util.CustomSslSocketFactory;
import io.kafbat.ui.util.StaticFileWebFilter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -47,6 +49,9 @@
@RequiredArgsConstructor
@Slf4j
public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
private static final Map<String, Object> BASE_ENV_PROPS = Map.of(
"java.naming.ldap.factory.socket", CustomSslSocketFactory.class.getName()
);

private final LdapProperties props;

Expand All @@ -70,6 +75,11 @@ public AbstractLdapAuthenticationProvider authenticationProvider(LdapAuthorities
props.getUrls());
wernerdv marked this conversation as resolved.
Show resolved Hide resolved
authProvider.setUseAuthenticationRequestCredentials(true);
((ActiveDirectoryLdapAuthenticationProvider) authProvider).setAuthoritiesPopulator(authoritiesExtractor);

List<String> urls = List.of(props.getUrls().split(","));
wernerdv marked this conversation as resolved.
Show resolved Hide resolved
if (urls.stream().anyMatch(url -> url.startsWith("ldaps://"))) {
((ActiveDirectoryLdapAuthenticationProvider) authProvider).setContextEnvironmentProperties(BASE_ENV_PROPS);
}
}

if (rbacEnabled) {
Expand Down
90 changes: 90 additions & 0 deletions api/src/main/java/io/kafbat/ui/util/CustomSslSocketFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.kafbat.ui.util;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class CustomSslSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory socketFactory;

public CustomSslSocketFactory() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[] { new DisabledX509TrustManager() }, new SecureRandom());
Haarolean marked this conversation as resolved.
Show resolved Hide resolved
socketFactory = ctx.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static SocketFactory getDefault() {
return new CustomSslSocketFactory();
}

@Override
public String[] getDefaultCipherSuites() {
return socketFactory.getDefaultCipherSuites();
}

@Override
public String[] getSupportedCipherSuites() {
return socketFactory.getSupportedCipherSuites();
}

@Override
public Socket createSocket(Socket socket, String string, int i, boolean bln) throws IOException {
return socketFactory.createSocket(socket, string, i, bln);
}

@Override
public Socket createSocket(String string, int i) throws IOException {
return socketFactory.createSocket(string, i);
}

@Override
public Socket createSocket(String string, int i, InetAddress ia, int i1) throws IOException {
return socketFactory.createSocket(string, i, ia, i1);
}

@Override
public Socket createSocket(InetAddress ia, int i) throws IOException {
return socketFactory.createSocket(ia, i);
}

@Override
public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1) throws IOException {
return socketFactory.createSocket(ia, i, ia1, i1);
}

@Override
public Socket createSocket() throws IOException {
return socketFactory.createSocket();
}

private static class DisabledX509TrustManager implements X509TrustManager {
/** Empty certificate array. */
private static final X509Certificate[] CERTS = new X509Certificate[0];

@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
Dismissed Show dismissed Hide dismissed
// No-op, all clients are trusted.
}

@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
Dismissed Show dismissed Hide dismissed
// No-op, all servers are trusted.
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return CERTS;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.kafbat.ui;

import static io.kafbat.ui.AbstractIntegrationTest.LOCAL;
import static io.kafbat.ui.container.ActiveDirectoryContainer.DOMAIN;
import static io.kafbat.ui.container.ActiveDirectoryContainer.EMPTY_PERMISSIONS_USER;
import static io.kafbat.ui.container.ActiveDirectoryContainer.FIRST_USER_WITH_GROUP;
import static io.kafbat.ui.container.ActiveDirectoryContainer.PASSWORD;
Expand All @@ -12,49 +11,29 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.kafbat.ui.container.ActiveDirectoryContainer;
import io.kafbat.ui.model.AuthenticationInfoDTO;
import io.kafbat.ui.model.ResourceTypeDTO;
import io.kafbat.ui.model.UserPermissionDTO;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;

@SpringBootTest
@ActiveProfiles("rbac-ad")
@AutoConfigureWebTestClient(timeout = "60000")
@ContextConfiguration(initializers = {ActiveDirectoryIntegrationTest.Initializer.class})
public class ActiveDirectoryIntegrationTest {
public abstract class AbstractActiveDirectoryIntegrationTest {
private static final String SESSION = "SESSION";

private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer();

@Autowired
private WebTestClient webTestClient;

@BeforeAll
public static void setup() {
ACTIVE_DIRECTORY.start();
}

@AfterAll
public static void shutdown() {
ACTIVE_DIRECTORY.stop();
}

@Test
public void testUserPermissions() {
AuthenticationInfoDTO info = authenticationInfo(FIRST_USER_WITH_GROUP);
Expand Down Expand Up @@ -108,13 +87,4 @@ private AuthenticationInfoDTO authenticationInfo(String name) {
.getResponseBody()
.blockFirst();
}

public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(@NotNull ConfigurableApplicationContext context) {
System.setProperty("spring.ldap.urls", ACTIVE_DIRECTORY.getLdapUrl());
System.setProperty("oauth2.ldap.activeDirectory", "true");
System.setProperty("oauth2.ldap.activeDirectory.domain", DOMAIN);
}
}
}
35 changes: 35 additions & 0 deletions api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.kafbat.ui;

import static io.kafbat.ui.container.ActiveDirectoryContainer.DOMAIN;

import io.kafbat.ui.container.ActiveDirectoryContainer;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;

@ContextConfiguration(initializers = {ActiveDirectoryLdapTest.Initializer.class})
public class ActiveDirectoryLdapTest extends AbstractActiveDirectoryIntegrationTest {
wernerdv marked this conversation as resolved.
Show resolved Hide resolved
private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer(false);

@BeforeAll
public static void setup() {
ACTIVE_DIRECTORY.start();
}

@AfterAll
public static void shutdown() {
ACTIVE_DIRECTORY.stop();
}

public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(@NotNull ConfigurableApplicationContext context) {
System.setProperty("spring.ldap.urls", ACTIVE_DIRECTORY.getLdapUrl());
System.setProperty("oauth2.ldap.activeDirectory", "true");
System.setProperty("oauth2.ldap.activeDirectory.domain", DOMAIN);
}
}
}
100 changes: 100 additions & 0 deletions api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.kafbat.ui;

import static io.kafbat.ui.container.ActiveDirectoryContainer.CONTAINER_CERT_PATH;
import static io.kafbat.ui.container.ActiveDirectoryContainer.CONTAINER_KEY_PATH;
import static io.kafbat.ui.container.ActiveDirectoryContainer.DOMAIN;
import static io.kafbat.ui.container.ActiveDirectoryContainer.PASSWORD;
import static org.testcontainers.utility.MountableFile.forHostPath;

import io.kafbat.ui.container.ActiveDirectoryContainer;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Map;
import org.apache.kafka.common.config.types.Password;
import org.apache.kafka.test.TestSslUtils;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemWriter;

@ContextConfiguration(initializers = {ActiveDirectoryLdapsTest.Initializer.class})
public class ActiveDirectoryLdapsTest extends AbstractActiveDirectoryIntegrationTest {
private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer(true);

private static File certPem = null;
private static File privateKeyPem = null;

@BeforeAll
public static void setup() throws Exception {
generateCerts();

ACTIVE_DIRECTORY.withCopyFileToContainer(forHostPath(certPem.getAbsolutePath()), CONTAINER_CERT_PATH);
ACTIVE_DIRECTORY.withCopyFileToContainer(forHostPath(privateKeyPem.getAbsolutePath()), CONTAINER_KEY_PATH);

ACTIVE_DIRECTORY.start();
}

@AfterAll
public static void shutdown() {
ACTIVE_DIRECTORY.stop();
}

private static void generateCerts() throws Exception {
File truststore = File.createTempFile("truststore", ".jks");

truststore.deleteOnExit();

String host = "localhost";
KeyPair clientKeyPair = TestSslUtils.generateKeyPair("RSA");

X509Certificate clientCert = new TestSslUtils.CertificateBuilder(365, "SHA256withRSA")
.sanDnsNames(host)
.sanIpAddress(InetAddress.getByName(host))
.generate("O=Samba Administration, OU=Samba, CN=" + host, clientKeyPair);

TestSslUtils.createTrustStore(truststore.getPath(), new Password(PASSWORD), Map.of("client", clientCert));

certPem = File.createTempFile("cert", ".pem");
wernerdv marked this conversation as resolved.
Show resolved Hide resolved
try (FileWriter fw = new FileWriter(certPem)) {
fw.write(certOrKeyToString(clientCert));
}

privateKeyPem = File.createTempFile("key", ".pem");
try (FileWriter fw = new FileWriter(privateKeyPem)) {
fw.write(certOrKeyToString(clientKeyPair.getPrivate()));
}
}

private static String certOrKeyToString(Object certOrKey) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
wernerdv marked this conversation as resolved.
Show resolved Hide resolved
if (certOrKey instanceof X509Certificate) {
pemWriter.writeObject(new JcaMiscPEMGenerator(certOrKey));
} else {
pemWriter.writeObject(new JcaPKCS8Generator((PrivateKey) certOrKey, null));
}
}
return out.toString(StandardCharsets.UTF_8);
}

public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(@NotNull ConfigurableApplicationContext context) {
System.setProperty("spring.ldap.urls", ACTIVE_DIRECTORY.getLdapUrl());
System.setProperty("oauth2.ldap.activeDirectory", "true");
System.setProperty("oauth2.ldap.activeDirectory.domain", DOMAIN);
}
}
}
Loading
Loading