Skip to content

Commit 4b729e0

Browse files
committed
BE: RBAC: Support LDAPS for AD
1 parent a05709f commit 4b729e0

File tree

7 files changed

+289
-35
lines changed

7 files changed

+289
-35
lines changed

api/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,19 @@
212212
<version>${okhttp3.mockwebserver.version}</version>
213213
<scope>test</scope>
214214
</dependency>
215+
<dependency>
216+
<groupId>org.apache.kafka</groupId>
217+
<artifactId>kafka-clients</artifactId>
218+
<version>${confluent.version}-ccs</version>
219+
<classifier>test</classifier>
220+
<scope>test</scope>
221+
</dependency>
222+
<dependency>
223+
<groupId>org.bouncycastle</groupId>
224+
<artifactId>bcpkix-jdk18on</artifactId>
225+
<version>1.80</version>
226+
<scope>test</scope>
227+
</dependency>
215228

216229
<dependency>
217230
<groupId>org.springframework.boot</groupId>

api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import io.kafbat.ui.service.rbac.AccessControlService;
44
import io.kafbat.ui.service.rbac.extractor.RbacActiveDirectoryAuthoritiesExtractor;
55
import io.kafbat.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
6+
import io.kafbat.ui.util.CustomSslSocketFactory;
67
import io.kafbat.ui.util.StaticFileWebFilter;
78
import java.util.Collection;
89
import java.util.List;
10+
import java.util.Map;
911
import java.util.Optional;
1012
import lombok.RequiredArgsConstructor;
1113
import lombok.extern.slf4j.Slf4j;
@@ -47,6 +49,9 @@
4749
@RequiredArgsConstructor
4850
@Slf4j
4951
public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
52+
private static final Map<String, Object> BASE_ENV_PROPS = Map.of(
53+
"java.naming.ldap.factory.socket", CustomSslSocketFactory.class.getName()
54+
);
5055

5156
private final LdapProperties props;
5257

@@ -70,6 +75,11 @@ public AbstractLdapAuthenticationProvider authenticationProvider(LdapAuthorities
7075
props.getUrls());
7176
authProvider.setUseAuthenticationRequestCredentials(true);
7277
((ActiveDirectoryLdapAuthenticationProvider) authProvider).setAuthoritiesPopulator(authoritiesExtractor);
78+
79+
List<String> urls = List.of(props.getUrls().split(","));
80+
if (urls.stream().anyMatch(url -> url.startsWith("ldaps://"))) {
81+
((ActiveDirectoryLdapAuthenticationProvider) authProvider).setContextEnvironmentProperties(BASE_ENV_PROPS);
82+
}
7383
}
7484

7585
if (rbacEnabled) {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.kafbat.ui.util;
2+
3+
import java.io.IOException;
4+
import java.net.InetAddress;
5+
import java.net.Socket;
6+
import java.security.SecureRandom;
7+
import java.security.cert.X509Certificate;
8+
import javax.net.SocketFactory;
9+
import javax.net.ssl.SSLContext;
10+
import javax.net.ssl.SSLSocketFactory;
11+
import javax.net.ssl.TrustManager;
12+
import javax.net.ssl.X509TrustManager;
13+
14+
public class CustomSslSocketFactory extends SSLSocketFactory {
15+
private final SSLSocketFactory socketFactory;
16+
17+
public CustomSslSocketFactory() {
18+
try {
19+
SSLContext ctx = SSLContext.getInstance("TLS");
20+
ctx.init(null, new TrustManager[] { new DisabledX509TrustManager() }, new SecureRandom());
21+
socketFactory = ctx.getSocketFactory();
22+
} catch (Exception e) {
23+
throw new RuntimeException(e);
24+
}
25+
}
26+
27+
public static SocketFactory getDefault() {
28+
return new CustomSslSocketFactory();
29+
}
30+
31+
@Override
32+
public String[] getDefaultCipherSuites() {
33+
return socketFactory.getDefaultCipherSuites();
34+
}
35+
36+
@Override
37+
public String[] getSupportedCipherSuites() {
38+
return socketFactory.getSupportedCipherSuites();
39+
}
40+
41+
@Override
42+
public Socket createSocket(Socket socket, String string, int i, boolean bln) throws IOException {
43+
return socketFactory.createSocket(socket, string, i, bln);
44+
}
45+
46+
@Override
47+
public Socket createSocket(String string, int i) throws IOException {
48+
return socketFactory.createSocket(string, i);
49+
}
50+
51+
@Override
52+
public Socket createSocket(String string, int i, InetAddress ia, int i1) throws IOException {
53+
return socketFactory.createSocket(string, i, ia, i1);
54+
}
55+
56+
@Override
57+
public Socket createSocket(InetAddress ia, int i) throws IOException {
58+
return socketFactory.createSocket(ia, i);
59+
}
60+
61+
@Override
62+
public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1) throws IOException {
63+
return socketFactory.createSocket(ia, i, ia1, i1);
64+
}
65+
66+
@Override
67+
public Socket createSocket() throws IOException {
68+
return socketFactory.createSocket();
69+
}
70+
71+
private static class DisabledX509TrustManager implements X509TrustManager {
72+
/** Empty certificate array. */
73+
private static final X509Certificate[] CERTS = new X509Certificate[0];
74+
75+
@Override
76+
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
77+
// No-op, all clients are trusted.
78+
}
79+
80+
@Override
81+
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
82+
// No-op, all servers are trusted.
83+
}
84+
85+
@Override
86+
public X509Certificate[] getAcceptedIssuers() {
87+
return CERTS;
88+
}
89+
}
90+
}

api/src/test/java/io/kafbat/ui/ActiveDirectoryIntegrationTest.java renamed to api/src/test/java/io/kafbat/ui/AbstractActiveDirectoryIntegrationTest.java

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.kafbat.ui;
22

33
import static io.kafbat.ui.AbstractIntegrationTest.LOCAL;
4-
import static io.kafbat.ui.container.ActiveDirectoryContainer.DOMAIN;
54
import static io.kafbat.ui.container.ActiveDirectoryContainer.EMPTY_PERMISSIONS_USER;
65
import static io.kafbat.ui.container.ActiveDirectoryContainer.FIRST_USER_WITH_GROUP;
76
import static io.kafbat.ui.container.ActiveDirectoryContainer.PASSWORD;
@@ -12,49 +11,29 @@
1211
import static org.junit.jupiter.api.Assertions.assertNotNull;
1312
import static org.junit.jupiter.api.Assertions.assertTrue;
1413

15-
import io.kafbat.ui.container.ActiveDirectoryContainer;
1614
import io.kafbat.ui.model.AuthenticationInfoDTO;
1715
import io.kafbat.ui.model.ResourceTypeDTO;
1816
import io.kafbat.ui.model.UserPermissionDTO;
1917
import java.util.List;
2018
import java.util.Objects;
21-
import org.jetbrains.annotations.NotNull;
22-
import org.junit.jupiter.api.AfterAll;
23-
import org.junit.jupiter.api.BeforeAll;
2419
import org.junit.jupiter.api.Test;
2520
import org.springframework.beans.factory.annotation.Autowired;
2621
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
2722
import org.springframework.boot.test.context.SpringBootTest;
28-
import org.springframework.context.ApplicationContextInitializer;
29-
import org.springframework.context.ConfigurableApplicationContext;
3023
import org.springframework.http.MediaType;
3124
import org.springframework.test.context.ActiveProfiles;
32-
import org.springframework.test.context.ContextConfiguration;
3325
import org.springframework.test.web.reactive.server.WebTestClient;
3426
import org.springframework.web.reactive.function.BodyInserters;
3527

3628
@SpringBootTest
3729
@ActiveProfiles("rbac-ad")
3830
@AutoConfigureWebTestClient(timeout = "60000")
39-
@ContextConfiguration(initializers = {ActiveDirectoryIntegrationTest.Initializer.class})
40-
public class ActiveDirectoryIntegrationTest {
31+
public abstract class AbstractActiveDirectoryIntegrationTest {
4132
private static final String SESSION = "SESSION";
4233

43-
private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer();
44-
4534
@Autowired
4635
private WebTestClient webTestClient;
4736

48-
@BeforeAll
49-
public static void setup() {
50-
ACTIVE_DIRECTORY.start();
51-
}
52-
53-
@AfterAll
54-
public static void shutdown() {
55-
ACTIVE_DIRECTORY.stop();
56-
}
57-
5837
@Test
5938
public void testUserPermissions() {
6039
AuthenticationInfoDTO info = authenticationInfo(FIRST_USER_WITH_GROUP);
@@ -108,13 +87,4 @@ private AuthenticationInfoDTO authenticationInfo(String name) {
10887
.getResponseBody()
10988
.blockFirst();
11089
}
111-
112-
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
113-
@Override
114-
public void initialize(@NotNull ConfigurableApplicationContext context) {
115-
System.setProperty("spring.ldap.urls", ACTIVE_DIRECTORY.getLdapUrl());
116-
System.setProperty("oauth2.ldap.activeDirectory", "true");
117-
System.setProperty("oauth2.ldap.activeDirectory.domain", DOMAIN);
118-
}
119-
}
12090
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.kafbat.ui;
2+
3+
import static io.kafbat.ui.container.ActiveDirectoryContainer.DOMAIN;
4+
5+
import io.kafbat.ui.container.ActiveDirectoryContainer;
6+
import org.jetbrains.annotations.NotNull;
7+
import org.junit.jupiter.api.AfterAll;
8+
import org.junit.jupiter.api.BeforeAll;
9+
import org.springframework.context.ApplicationContextInitializer;
10+
import org.springframework.context.ConfigurableApplicationContext;
11+
import org.springframework.test.context.ContextConfiguration;
12+
13+
@ContextConfiguration(initializers = {ActiveDirectoryLdapTest.Initializer.class})
14+
public class ActiveDirectoryLdapTest extends AbstractActiveDirectoryIntegrationTest {
15+
private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer(false);
16+
17+
@BeforeAll
18+
public static void setup() {
19+
ACTIVE_DIRECTORY.start();
20+
}
21+
22+
@AfterAll
23+
public static void shutdown() {
24+
ACTIVE_DIRECTORY.stop();
25+
}
26+
27+
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
28+
@Override
29+
public void initialize(@NotNull ConfigurableApplicationContext context) {
30+
System.setProperty("spring.ldap.urls", ACTIVE_DIRECTORY.getLdapUrl());
31+
System.setProperty("oauth2.ldap.activeDirectory", "true");
32+
System.setProperty("oauth2.ldap.activeDirectory.domain", DOMAIN);
33+
}
34+
}
35+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package io.kafbat.ui;
2+
3+
import static io.kafbat.ui.container.ActiveDirectoryContainer.CONTAINER_CERT_PATH;
4+
import static io.kafbat.ui.container.ActiveDirectoryContainer.CONTAINER_KEY_PATH;
5+
import static io.kafbat.ui.container.ActiveDirectoryContainer.DOMAIN;
6+
import static io.kafbat.ui.container.ActiveDirectoryContainer.PASSWORD;
7+
import static org.testcontainers.utility.MountableFile.forHostPath;
8+
9+
import io.kafbat.ui.container.ActiveDirectoryContainer;
10+
import java.io.ByteArrayOutputStream;
11+
import java.io.File;
12+
import java.io.FileWriter;
13+
import java.io.OutputStreamWriter;
14+
import java.net.InetAddress;
15+
import java.nio.charset.StandardCharsets;
16+
import java.security.KeyPair;
17+
import java.security.PrivateKey;
18+
import java.security.cert.X509Certificate;
19+
import java.util.Map;
20+
import org.apache.kafka.common.config.types.Password;
21+
import org.apache.kafka.test.TestSslUtils;
22+
import org.jetbrains.annotations.NotNull;
23+
import org.junit.jupiter.api.AfterAll;
24+
import org.junit.jupiter.api.BeforeAll;
25+
import org.springframework.context.ApplicationContextInitializer;
26+
import org.springframework.context.ConfigurableApplicationContext;
27+
import org.springframework.test.context.ContextConfiguration;
28+
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
29+
import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
30+
import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemWriter;
31+
32+
@ContextConfiguration(initializers = {ActiveDirectoryLdapsTest.Initializer.class})
33+
public class ActiveDirectoryLdapsTest extends AbstractActiveDirectoryIntegrationTest {
34+
private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer(true);
35+
36+
private static File CERT_PEM = null;
37+
private static File PRIVATE_KEY_PEM = null;
38+
39+
@BeforeAll
40+
public static void setup() throws Exception {
41+
generateCerts();
42+
43+
ACTIVE_DIRECTORY.withCopyFileToContainer(forHostPath(CERT_PEM.getAbsolutePath()), CONTAINER_CERT_PATH);
44+
ACTIVE_DIRECTORY.withCopyFileToContainer(forHostPath(PRIVATE_KEY_PEM.getAbsolutePath()), CONTAINER_KEY_PATH);
45+
46+
ACTIVE_DIRECTORY.start();
47+
}
48+
49+
@AfterAll
50+
public static void shutdown() {
51+
ACTIVE_DIRECTORY.stop();
52+
}
53+
54+
private static void generateCerts() throws Exception {
55+
File truststore = File.createTempFile("truststore", ".jks");
56+
57+
truststore.deleteOnExit();
58+
59+
String host = "localhost";
60+
KeyPair clientKeyPair = TestSslUtils.generateKeyPair("RSA");
61+
62+
X509Certificate clientCert = new TestSslUtils.CertificateBuilder(365, "SHA256withRSA")
63+
.sanDnsNames(host)
64+
.sanIpAddress(InetAddress.getByName(host))
65+
.generate("O=Samba Administration, OU=Samba, CN=" + host, clientKeyPair);
66+
67+
TestSslUtils.createTrustStore(truststore.getPath(), new Password(PASSWORD), Map.of("client", clientCert));
68+
69+
CERT_PEM = File.createTempFile("cert", ".pem");
70+
try (FileWriter fw = new FileWriter(CERT_PEM)) {
71+
fw.write(certOrKeyToString(clientCert));
72+
}
73+
74+
PRIVATE_KEY_PEM = File.createTempFile("key", ".pem");
75+
try (FileWriter fw = new FileWriter(PRIVATE_KEY_PEM)) {
76+
fw.write(certOrKeyToString(clientKeyPair.getPrivate()));
77+
}
78+
}
79+
80+
private static String certOrKeyToString(Object certOrKey) throws Exception {
81+
ByteArrayOutputStream out = new ByteArrayOutputStream();
82+
try (PemWriter pemWriter = new PemWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
83+
if (certOrKey instanceof X509Certificate) {
84+
pemWriter.writeObject(new JcaMiscPEMGenerator(certOrKey));
85+
} else {
86+
pemWriter.writeObject(new JcaPKCS8Generator((PrivateKey) certOrKey, null));
87+
}
88+
}
89+
return out.toString(StandardCharsets.UTF_8);
90+
}
91+
92+
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
93+
@Override
94+
public void initialize(@NotNull ConfigurableApplicationContext context) {
95+
System.setProperty("spring.ldap.urls", ACTIVE_DIRECTORY.getLdapUrl());
96+
System.setProperty("oauth2.ldap.activeDirectory", "true");
97+
System.setProperty("oauth2.ldap.activeDirectory.domain", DOMAIN);
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)