diff --git a/api/pom.xml b/api/pom.xml
index dd2c3a378..fac019ab5 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -212,6 +212,19 @@
${okhttp3.mockwebserver.version}
test
+
+ org.apache.kafka
+ kafka-clients
+ ${confluent.version}-ccs
+ test
+ test
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ 1.80
+ test
+
org.springframework.boot
diff --git a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java
index 4b7473942..4267a4b0e 100644
--- a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java
+++ b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java
@@ -3,10 +3,13 @@
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 java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,6 +50,9 @@
@RequiredArgsConstructor
@Slf4j
public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
+ private static final Map BASE_ENV_PROPS = Map.of(
+ "java.naming.ldap.factory.socket", CustomSslSocketFactory.class.getName()
+ );
private final LdapProperties props;
@@ -63,13 +69,10 @@ public AbstractLdapAuthenticationProvider authenticationProvider(LdapAuthorities
AbstractLdapAuthenticationProvider authProvider;
- if (!props.isActiveDirectory()) {
- authProvider = new LdapAuthenticationProvider(ba, authoritiesExtractor);
+ if (props.isActiveDirectory()) {
+ authProvider = activeDirectoryProvider(authoritiesExtractor);
} else {
- authProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
- props.getUrls());
- authProvider.setUseAuthenticationRequestCredentials(true);
- ((ActiveDirectoryLdapAuthenticationProvider) authProvider).setAuthoritiesPopulator(authoritiesExtractor);
+ authProvider = new LdapAuthenticationProvider(ba, authoritiesExtractor);
}
if (rbacEnabled) {
@@ -159,6 +162,22 @@ public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) {
return builder.build();
}
+ private ActiveDirectoryLdapAuthenticationProvider activeDirectoryProvider(LdapAuthoritiesPopulator populator) {
+ ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
+ props.getActiveDirectoryDomain(),
+ props.getUrls()
+ );
+
+ provider.setUseAuthenticationRequestCredentials(true);
+ provider.setAuthoritiesPopulator(populator);
+
+ if (Stream.of(props.getUrls().split(",")).anyMatch(url -> url.startsWith("ldaps://"))) {
+ provider.setContextEnvironmentProperties(BASE_ENV_PROPS);
+ }
+
+ return provider;
+ }
+
private static class RbacUserDetailsMapper extends LdapUserDetailsMapper {
@Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username,
diff --git a/api/src/main/java/io/kafbat/ui/util/CustomSslSocketFactory.java b/api/src/main/java/io/kafbat/ui/util/CustomSslSocketFactory.java
new file mode 100644
index 000000000..b5fee8cae
--- /dev/null
+++ b/api/src/main/java/io/kafbat/ui/util/CustomSslSocketFactory.java
@@ -0,0 +1,69 @@
+package io.kafbat.ui.util;
+
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.SecureRandom;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+public class CustomSslSocketFactory extends SSLSocketFactory {
+ private final SSLSocketFactory socketFactory;
+
+ public CustomSslSocketFactory() {
+ try {
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), new SecureRandom());
+
+ socketFactory = sslContext.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();
+ }
+}
diff --git a/api/src/test/java/io/kafbat/ui/ActiveDirectoryIntegrationTest.java b/api/src/test/java/io/kafbat/ui/AbstractActiveDirectoryIntegrationTest.java
similarity index 55%
rename from api/src/test/java/io/kafbat/ui/ActiveDirectoryIntegrationTest.java
rename to api/src/test/java/io/kafbat/ui/AbstractActiveDirectoryIntegrationTest.java
index 80c3abe33..098da29f9 100644
--- a/api/src/test/java/io/kafbat/ui/ActiveDirectoryIntegrationTest.java
+++ b/api/src/test/java/io/kafbat/ui/AbstractActiveDirectoryIntegrationTest.java
@@ -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;
@@ -12,52 +11,26 @@
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);
+ protected static void checkUserPermissions(WebTestClient client) {
+ AuthenticationInfoDTO info = authenticationInfo(client, FIRST_USER_WITH_GROUP);
assertNotNull(info);
assertTrue(info.getRbacEnabled());
@@ -67,22 +40,21 @@ public void testUserPermissions() {
assertFalse(permissions.isEmpty());
assertTrue(permissions.stream().anyMatch(permission ->
permission.getClusters().contains(LOCAL) && permission.getResource() == ResourceTypeDTO.TOPIC));
- assertEquals(permissions, authenticationInfo(SECOND_USER_WITH_GROUP).getUserInfo().getPermissions());
- assertEquals(permissions, authenticationInfo(USER_WITHOUT_GROUP).getUserInfo().getPermissions());
+ assertEquals(permissions, authenticationInfo(client, SECOND_USER_WITH_GROUP).getUserInfo().getPermissions());
+ assertEquals(permissions, authenticationInfo(client, USER_WITHOUT_GROUP).getUserInfo().getPermissions());
}
- @Test
- public void testEmptyPermissions() {
- assertTrue(Objects.requireNonNull(authenticationInfo(EMPTY_PERMISSIONS_USER))
+ protected static void checkEmptyPermissions(WebTestClient client) {
+ assertTrue(Objects.requireNonNull(authenticationInfo(client, EMPTY_PERMISSIONS_USER))
.getUserInfo()
.getPermissions()
.isEmpty()
);
}
- private String session(String name) {
+ protected static String session(WebTestClient client, String name) {
return Objects.requireNonNull(
- webTestClient
+ client
.post()
.uri("/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
@@ -96,11 +68,11 @@ private String session(String name) {
.getValue();
}
- private AuthenticationInfoDTO authenticationInfo(String name) {
- return webTestClient
+ protected static AuthenticationInfoDTO authenticationInfo(WebTestClient client, String name) {
+ return client
.get()
.uri("/api/authorization")
- .cookie(SESSION, session(name))
+ .cookie(SESSION, session(client, name))
.exchange()
.expectStatus()
.isOk()
@@ -108,13 +80,4 @@ private AuthenticationInfoDTO authenticationInfo(String name) {
.getResponseBody()
.blockFirst();
}
-
- public static class Initializer implements ApplicationContextInitializer {
- @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);
- }
- }
}
diff --git a/api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapTest.java b/api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapTest.java
new file mode 100644
index 000000000..4d32513f7
--- /dev/null
+++ b/api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapTest.java
@@ -0,0 +1,51 @@
+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.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+@ContextConfiguration(initializers = {ActiveDirectoryLdapTest.Initializer.class})
+public class ActiveDirectoryLdapTest extends AbstractActiveDirectoryIntegrationTest {
+ private static final ActiveDirectoryContainer ACTIVE_DIRECTORY = new ActiveDirectoryContainer(false);
+
+ @Autowired
+ private WebTestClient webTestClient;
+
+ @BeforeAll
+ public static void setup() {
+ ACTIVE_DIRECTORY.start();
+ }
+
+ @AfterAll
+ public static void shutdown() {
+ ACTIVE_DIRECTORY.stop();
+ }
+
+ @Test
+ public void testUserPermissions() {
+ checkUserPermissions(webTestClient);
+ }
+
+ @Test
+ public void testEmptyPermissions() {
+ checkEmptyPermissions(webTestClient);
+ }
+
+ public static class Initializer implements ApplicationContextInitializer {
+ @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);
+ }
+ }
+}
diff --git a/api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapsTest.java b/api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapsTest.java
new file mode 100644
index 000000000..86d46961d
--- /dev/null
+++ b/api/src/test/java/io/kafbat/ui/ActiveDirectoryLdapsTest.java
@@ -0,0 +1,110 @@
+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 java.nio.file.Files.writeString;
+import static org.testcontainers.utility.MountableFile.forHostPath;
+
+import io.kafbat.ui.container.ActiveDirectoryContainer;
+import java.io.File;
+import java.io.StringWriter;
+import java.net.InetAddress;
+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.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.web.reactive.server.WebTestClient;
+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;
+
+ @Autowired
+ private WebTestClient webTestClient;
+
+ @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();
+ }
+
+ @Test
+ public void testUserPermissions() {
+ checkUserPermissions(webTestClient);
+ }
+
+ @Test
+ public void testEmptyPermissions() {
+ checkEmptyPermissions(webTestClient);
+ }
+
+ 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");
+ writeString(certPem.toPath(), certOrKeyToString(clientCert));
+
+ privateKeyPem = File.createTempFile("key", ".pem");
+ writeString(privateKeyPem.toPath(), certOrKeyToString(clientKeyPair.getPrivate()));
+ }
+
+ private static String certOrKeyToString(Object certOrKey) throws Exception {
+ StringWriter sw = new StringWriter();
+ try (PemWriter pw = new PemWriter(sw)) {
+ if (certOrKey instanceof X509Certificate) {
+ pw.writeObject(new JcaMiscPEMGenerator(certOrKey));
+ } else {
+ pw.writeObject(new JcaPKCS8Generator((PrivateKey) certOrKey, null));
+ }
+ }
+ return sw.toString();
+ }
+
+ public static class Initializer implements ApplicationContextInitializer {
+ @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);
+ }
+ }
+}
diff --git a/api/src/test/java/io/kafbat/ui/container/ActiveDirectoryContainer.java b/api/src/test/java/io/kafbat/ui/container/ActiveDirectoryContainer.java
index 55bc3a186..af1f42a68 100644
--- a/api/src/test/java/io/kafbat/ui/container/ActiveDirectoryContainer.java
+++ b/api/src/test/java/io/kafbat/ui/container/ActiveDirectoryContainer.java
@@ -14,6 +14,8 @@ public class ActiveDirectoryContainer extends GenericContainer