mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-01 11:09:14 +00:00
LDAP login issues fixed, added retry mechanism (#23690)
* LDAP login issues fixed, added retry mechanism * Remove unwanted comments * Addressed copilot comments * Added integration TCs for ldap
This commit is contained in:
parent
e23638da60
commit
9d0a739c69
@ -1,5 +1,8 @@
|
|||||||
package org.openmetadata.service.security.auth;
|
package org.openmetadata.service.security.auth;
|
||||||
|
|
||||||
|
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
|
||||||
|
import static jakarta.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE;
|
||||||
|
import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;
|
||||||
import static org.openmetadata.service.security.SecurityUtil.writeJsonResponse;
|
import static org.openmetadata.service.security.SecurityUtil.writeJsonResponse;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@ -16,6 +19,7 @@ import org.openmetadata.schema.auth.TokenRefreshRequest;
|
|||||||
import org.openmetadata.schema.utils.JsonUtils;
|
import org.openmetadata.schema.utils.JsonUtils;
|
||||||
import org.openmetadata.service.OpenMetadataApplicationConfig;
|
import org.openmetadata.service.OpenMetadataApplicationConfig;
|
||||||
import org.openmetadata.service.auth.JwtResponse;
|
import org.openmetadata.service.auth.JwtResponse;
|
||||||
|
import org.openmetadata.service.exception.CustomExceptionMessage;
|
||||||
import org.openmetadata.service.security.AuthServeletHandler;
|
import org.openmetadata.service.security.AuthServeletHandler;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -109,9 +113,21 @@ public class LdapAuthServletHandler implements AuthServeletHandler {
|
|||||||
resp.setContentType("application/json");
|
resp.setContentType("application/json");
|
||||||
writeJsonResponse(resp, JsonUtils.pojoToJson(responseToClient));
|
writeJsonResponse(resp, JsonUtils.pojoToJson(responseToClient));
|
||||||
|
|
||||||
|
} catch (CustomExceptionMessage e) {
|
||||||
|
LOG.error("LDAP login error: {}", e.getMessage(), e);
|
||||||
|
int statusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||||
|
if (e.getResponse().getStatus() == SERVICE_UNAVAILABLE.getStatusCode()) {
|
||||||
|
statusCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
|
||||||
|
} else if (e.getResponse().getStatus() == UNAUTHORIZED.getStatusCode()) {
|
||||||
|
statusCode = HttpServletResponse.SC_UNAUTHORIZED;
|
||||||
|
} else if (e.getResponse().getStatus() == FORBIDDEN.getStatusCode()) {
|
||||||
|
statusCode = HttpServletResponse.SC_FORBIDDEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendError(resp, statusCode, e.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Error handling LDAP login", e);
|
LOG.error("Unexpected error handling LDAP login", e);
|
||||||
sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
|
sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Authentication service error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +241,7 @@ public class LdapAuthServletHandler implements AuthServeletHandler {
|
|||||||
private void sendError(HttpServletResponse resp, int status, String message) {
|
private void sendError(HttpServletResponse resp, int status, String message) {
|
||||||
try {
|
try {
|
||||||
resp.setStatus(status);
|
resp.setStatus(status);
|
||||||
writeJsonResponse(resp, String.format("{\"error\":\"%s\"}", message));
|
writeJsonResponse(resp, String.format("{\"message\":\"%s\"}", message));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Error writing error response", e);
|
LOG.error("Error writing error response", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package org.openmetadata.service.security.auth;
|
|||||||
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
|
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
|
||||||
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
|
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
|
||||||
import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
|
import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
|
||||||
|
import static jakarta.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE;
|
||||||
import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;
|
import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;
|
||||||
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
|
||||||
import static org.openmetadata.schema.auth.TokenType.REFRESH_TOKEN;
|
import static org.openmetadata.schema.auth.TokenType.REFRESH_TOKEN;
|
||||||
@ -81,6 +82,8 @@ import org.springframework.util.CollectionUtils;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class LdapAuthenticator implements AuthenticatorHandler {
|
public class LdapAuthenticator implements AuthenticatorHandler {
|
||||||
static final String LDAP_ERR_MSG = "[LDAP] Issue in creating a LookUp Connection ";
|
static final String LDAP_ERR_MSG = "[LDAP] Issue in creating a LookUp Connection ";
|
||||||
|
private static final int MAX_RETRIES = 3;
|
||||||
|
private static final int BASE_DELAY_MS = 500;
|
||||||
private RoleRepository roleRepository;
|
private RoleRepository roleRepository;
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
private TokenRepository tokenRepository;
|
private TokenRepository tokenRepository;
|
||||||
@ -197,29 +200,74 @@ public class LdapAuthenticator implements AuthenticatorHandler {
|
|||||||
@Override
|
@Override
|
||||||
public void validatePassword(String userDn, String reqPassword, User dummy)
|
public void validatePassword(String userDn, String reqPassword, User dummy)
|
||||||
throws TemplateException, IOException {
|
throws TemplateException, IOException {
|
||||||
// performed in LDAP , the storedUser's name set as DN of the User in Ldap
|
// Retry configuration for connection establishment
|
||||||
|
final int maxRetries = 3;
|
||||||
|
final int baseDelayMs = 500;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
performLdapBind(userDn, reqPassword, dummy);
|
||||||
|
return; // Success
|
||||||
|
} catch (CustomExceptionMessage e) {
|
||||||
|
handleRetryableException(e, attempt, "LDAP connection failed for authentication");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
SERVICE_UNAVAILABLE,
|
||||||
|
"LDAP_CONNECTION_ERROR",
|
||||||
|
"Unable to connect to authentication server after " + maxRetries + " attempts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performLdapBind(String userDn, String reqPassword, User dummy)
|
||||||
|
throws TemplateException, IOException {
|
||||||
BindResult bindingResult = null;
|
BindResult bindingResult = null;
|
||||||
|
LDAPConnection userConnection = null;
|
||||||
try {
|
try {
|
||||||
bindingResult = ldapLookupConnectionPool.bind(userDn, reqPassword);
|
// Create a new connection for user authentication with proper SSL/TLS support
|
||||||
|
if (Boolean.TRUE.equals(ldapConfiguration.getSslEnabled())) {
|
||||||
|
// LDAPS (LDAP over SSL) - same configuration as connection pool
|
||||||
|
LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions();
|
||||||
|
LdapUtil ldapUtil = new LdapUtil();
|
||||||
|
SSLUtil sslUtil =
|
||||||
|
new SSLUtil(ldapUtil.getLdapSSLConnection(ldapConfiguration, connectionOptions));
|
||||||
|
userConnection =
|
||||||
|
new LDAPConnection(
|
||||||
|
sslUtil.createSSLSocketFactory(),
|
||||||
|
connectionOptions,
|
||||||
|
ldapConfiguration.getHost(),
|
||||||
|
ldapConfiguration.getPort());
|
||||||
|
} else {
|
||||||
|
userConnection =
|
||||||
|
new LDAPConnection(ldapConfiguration.getHost(), ldapConfiguration.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the bind operation
|
||||||
|
bindingResult = userConnection.bind(userDn, reqPassword);
|
||||||
if (Objects.equals(bindingResult.getResultCode().getName(), ResultCode.SUCCESS.getName())) {
|
if (Objects.equals(bindingResult.getResultCode().getName(), ResultCode.SUCCESS.getName())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
|
||||||
if (bindingResult != null
|
if (Objects.equals(
|
||||||
&& Objects.equals(
|
bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) {
|
||||||
bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) {
|
|
||||||
recordFailedLoginAttempt(dummy.getEmail(), dummy.getName());
|
recordFailedLoginAttempt(dummy.getEmail(), dummy.getName());
|
||||||
throw new CustomExceptionMessage(
|
throw new CustomExceptionMessage(
|
||||||
UNAUTHORIZED, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
|
UNAUTHORIZED, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
|
||||||
}
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
handleLdapException(ex);
|
||||||
|
} finally {
|
||||||
|
if (userConnection != null) {
|
||||||
|
userConnection.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (bindingResult != null) {
|
|
||||||
throw new CustomExceptionMessage(
|
LOG.error(
|
||||||
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, bindingResult.getResultCode().getName());
|
"LDAP bind failed with unexpected result: {}", bindingResult.getResultCode().getName());
|
||||||
} else {
|
throw new CustomExceptionMessage(
|
||||||
throw new CustomExceptionMessage(
|
INTERNAL_SERVER_ERROR,
|
||||||
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
|
"LDAP_AUTH_ERROR",
|
||||||
}
|
"Authentication failed: " + bindingResult.getResultCode().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -237,6 +285,24 @@ public class LdapAuthenticator implements AuthenticatorHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getUserDnFromLdap(String email) {
|
private String getUserDnFromLdap(String email) {
|
||||||
|
// Retry configuration (will be made configurable in future)
|
||||||
|
final int maxRetries = 3;
|
||||||
|
final int baseDelayMs = 500;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return performLdapUserSearch(email);
|
||||||
|
} catch (CustomExceptionMessage e) {
|
||||||
|
handleRetryableException(e, attempt, "LDAP connection failed for user lookup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
SERVICE_UNAVAILABLE,
|
||||||
|
"LDAP_CONNECTION_ERROR",
|
||||||
|
"Unable to connect to authentication server after " + maxRetries + " attempts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String performLdapUserSearch(String email) {
|
||||||
try {
|
try {
|
||||||
Filter emailFilter =
|
Filter emailFilter =
|
||||||
Filter.createEqualityFilter(ldapConfiguration.getMailAttributeName(), email);
|
Filter.createEqualityFilter(ldapConfiguration.getMailAttributeName(), email);
|
||||||
@ -270,7 +336,26 @@ public class LdapAuthenticator implements AuthenticatorHandler {
|
|||||||
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
|
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
|
||||||
}
|
}
|
||||||
} catch (LDAPException ex) {
|
} catch (LDAPException ex) {
|
||||||
throw new CustomExceptionMessage(INTERNAL_SERVER_ERROR, "LDAP_ERROR", ex.getMessage());
|
ResultCode resultCode = ex.getResultCode();
|
||||||
|
String errorMessage = ex.getMessage();
|
||||||
|
|
||||||
|
// Check if it's a connection/network error
|
||||||
|
if (resultCode == ResultCode.CONNECT_ERROR
|
||||||
|
|| resultCode == ResultCode.SERVER_DOWN
|
||||||
|
|| resultCode == ResultCode.UNAVAILABLE
|
||||||
|
|| resultCode == ResultCode.TIMEOUT
|
||||||
|
|| errorMessage.contains("Connection refused")
|
||||||
|
|| errorMessage.contains("Connection reset")
|
||||||
|
|| errorMessage.contains("No route to host")) {
|
||||||
|
LOG.error("LDAP connection error for user lookup: {}", errorMessage);
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
SERVICE_UNAVAILABLE,
|
||||||
|
"LDAP_CONNECTION_ERROR",
|
||||||
|
"Unable to connect to authentication server. Please try again later.");
|
||||||
|
}
|
||||||
|
LOG.error("LDAP error during user lookup: {}", errorMessage);
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
INTERNAL_SERVER_ERROR, "LDAP_ERROR", "Authentication server error: " + errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,4 +571,59 @@ public class LdapAuthenticator implements AuthenticatorHandler {
|
|||||||
|
|
||||||
return newRefreshToken;
|
return newRefreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleRetryableException(CustomExceptionMessage e, int attempt, String logMessage) {
|
||||||
|
if (e.getMessage().contains("Unable to connect to authentication server")
|
||||||
|
&& attempt < MAX_RETRIES) {
|
||||||
|
int delayMs = BASE_DELAY_MS * attempt;
|
||||||
|
LOG.warn(
|
||||||
|
"{} (attempt {}/{}). Retrying in {}ms...", logMessage, attempt, MAX_RETRIES, delayMs);
|
||||||
|
try {
|
||||||
|
Thread.sleep(delayMs);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.error("LDAP retry interrupted");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLdapException(Exception ex) throws TemplateException, IOException {
|
||||||
|
if (ex instanceof LDAPException ldapEx) {
|
||||||
|
ResultCode resultCode = ldapEx.getResultCode();
|
||||||
|
String errorMessage = ldapEx.getMessage();
|
||||||
|
|
||||||
|
// Check if it's a connection/network error
|
||||||
|
if (resultCode == ResultCode.CONNECT_ERROR
|
||||||
|
|| resultCode == ResultCode.SERVER_DOWN
|
||||||
|
|| resultCode == ResultCode.UNAVAILABLE
|
||||||
|
|| resultCode == ResultCode.TIMEOUT
|
||||||
|
|| errorMessage.contains("Connection refused")
|
||||||
|
|| errorMessage.contains("Connection reset")
|
||||||
|
|| errorMessage.contains("No route to host")) {
|
||||||
|
LOG.error("LDAP connection error during authentication: {}", errorMessage);
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
SERVICE_UNAVAILABLE,
|
||||||
|
"LDAP_CONNECTION_ERROR",
|
||||||
|
"Unable to connect to authentication server. Please try again later.");
|
||||||
|
}
|
||||||
|
LOG.error("LDAP error during authentication: {}", errorMessage);
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
INTERNAL_SERVER_ERROR, "LDAP_ERROR", "Authentication server error: " + errorMessage);
|
||||||
|
} else if (ex instanceof GeneralSecurityException) {
|
||||||
|
LOG.error("SSL/TLS error during LDAP authentication", ex);
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
INTERNAL_SERVER_ERROR,
|
||||||
|
"LDAP_SSL_ERROR",
|
||||||
|
"SSL/TLS configuration error: " + ex.getMessage());
|
||||||
|
} else {
|
||||||
|
LOG.error("Unexpected error during LDAP authentication", ex);
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
INTERNAL_SERVER_ERROR,
|
||||||
|
"LDAP_UNEXPECTED_ERROR",
|
||||||
|
"Unexpected authentication error: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,412 @@
|
|||||||
|
package org.openmetadata.service.security.auth;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.ws.rs.client.Entity;
|
||||||
|
import jakarta.ws.rs.client.Invocation;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.NewCookie;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.glassfish.jersey.client.ClientProperties;
|
||||||
|
import org.junit.jupiter.api.*;
|
||||||
|
import org.openmetadata.schema.auth.LoginRequest;
|
||||||
|
import org.openmetadata.service.OpenMetadataApplicationTest;
|
||||||
|
import org.openmetadata.service.util.TestUtils;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
class LdapAuthCompleteFlowTest extends OpenMetadataApplicationTest {
|
||||||
|
|
||||||
|
private static final String AUTH_LOGIN_ENDPOINT = "/api/v1/auth/login";
|
||||||
|
private static final String AUTH_LOGOUT_ENDPOINT = "/api/v1/auth/logout";
|
||||||
|
|
||||||
|
private static GenericContainer<?> ldapContainer;
|
||||||
|
private static String ldapHost;
|
||||||
|
private static int ldapPort;
|
||||||
|
|
||||||
|
private static final String TEST_USER_EMAIL = "testuser@testcompany.com";
|
||||||
|
private static final String TEST_USER_PASSWORD = "testpass123";
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
@Override
|
||||||
|
public void createApplication() throws Exception {
|
||||||
|
startLdapContainer();
|
||||||
|
super.createApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startLdapContainer() throws Exception {
|
||||||
|
LOG.info("Starting LDAP container...");
|
||||||
|
|
||||||
|
ldapContainer =
|
||||||
|
new GenericContainer<>(DockerImageName.parse("osixia/openldap:1.5.0"))
|
||||||
|
.withExposedPorts(389)
|
||||||
|
.withEnv("LDAP_ORGANISATION", "TestCompany")
|
||||||
|
.withEnv("LDAP_DOMAIN", "testcompany.com")
|
||||||
|
.withEnv("LDAP_ADMIN_PASSWORD", "adminpassword")
|
||||||
|
.withEnv("LDAP_CONFIG_PASSWORD", "config")
|
||||||
|
.withEnv("LDAP_READONLY_USER", "false")
|
||||||
|
.withEnv("LDAP_TLS", "false")
|
||||||
|
.withCommand("--copy-service", "--loglevel", "debug")
|
||||||
|
.withStartupTimeout(Duration.ofMinutes(2));
|
||||||
|
|
||||||
|
ldapContainer.start();
|
||||||
|
ldapHost = ldapContainer.getHost();
|
||||||
|
ldapPort = ldapContainer.getMappedPort(389);
|
||||||
|
|
||||||
|
LOG.info("LDAP container started at {}:{}", ldapHost, ldapPort);
|
||||||
|
|
||||||
|
Thread.sleep(10000);
|
||||||
|
|
||||||
|
loadTestLdapData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTestLdapData() throws Exception {
|
||||||
|
LOG.info("Loading test LDAP data...");
|
||||||
|
|
||||||
|
LOG.info("Step 1: Creating organizational units");
|
||||||
|
String ouLdif =
|
||||||
|
"dn: ou=users,dc=testcompany,dc=com\n"
|
||||||
|
+ "objectClass: organizationalUnit\n"
|
||||||
|
+ "ou: users\n\n"
|
||||||
|
+ "dn: ou=groups,dc=testcompany,dc=com\n"
|
||||||
|
+ "objectClass: organizationalUnit\n"
|
||||||
|
+ "ou: groups";
|
||||||
|
|
||||||
|
execLdapAdd(ouLdif, "organizational units");
|
||||||
|
|
||||||
|
LOG.info("Step 2: Creating test user");
|
||||||
|
String userLdif =
|
||||||
|
"dn: uid=testuser,ou=users,dc=testcompany,dc=com\n"
|
||||||
|
+ "objectClass: inetOrgPerson\n"
|
||||||
|
+ "objectClass: posixAccount\n"
|
||||||
|
+ "objectClass: shadowAccount\n"
|
||||||
|
+ "uid: testuser\n"
|
||||||
|
+ "sn: User\n"
|
||||||
|
+ "givenName: Test\n"
|
||||||
|
+ "cn: Test User\n"
|
||||||
|
+ "displayName: Test User\n"
|
||||||
|
+ "uidNumber: 10001\n"
|
||||||
|
+ "gidNumber: 5000\n"
|
||||||
|
+ "userPassword: testpass123\n"
|
||||||
|
+ "loginShell: /bin/bash\n"
|
||||||
|
+ "homeDirectory: /home/testuser\n"
|
||||||
|
+ "mail: testuser@testcompany.com";
|
||||||
|
|
||||||
|
execLdapAdd(userLdif, "test user");
|
||||||
|
|
||||||
|
LOG.info("Step 3: Creating admin user");
|
||||||
|
String adminLdif =
|
||||||
|
"dn: uid=admin,ou=users,dc=testcompany,dc=com\n"
|
||||||
|
+ "objectClass: inetOrgPerson\n"
|
||||||
|
+ "objectClass: posixAccount\n"
|
||||||
|
+ "objectClass: shadowAccount\n"
|
||||||
|
+ "uid: admin\n"
|
||||||
|
+ "sn: Admin\n"
|
||||||
|
+ "givenName: System\n"
|
||||||
|
+ "cn: System Admin\n"
|
||||||
|
+ "displayName: System Admin\n"
|
||||||
|
+ "uidNumber: 10000\n"
|
||||||
|
+ "gidNumber: 5000\n"
|
||||||
|
+ "userPassword: admin123\n"
|
||||||
|
+ "loginShell: /bin/bash\n"
|
||||||
|
+ "homeDirectory: /home/admin\n"
|
||||||
|
+ "mail: admin@testcompany.com";
|
||||||
|
|
||||||
|
execLdapAdd(adminLdif, "admin user");
|
||||||
|
|
||||||
|
LOG.info("Step 4: Creating groups");
|
||||||
|
String groupsLdif =
|
||||||
|
"dn: cn=DataConsumer,ou=groups,dc=testcompany,dc=com\n"
|
||||||
|
+ "objectClass: groupOfNames\n"
|
||||||
|
+ "cn: DataConsumer\n"
|
||||||
|
+ "description: Data Consumer group\n"
|
||||||
|
+ "member: uid=testuser,ou=users,dc=testcompany,dc=com\n\n"
|
||||||
|
+ "dn: cn=admin,ou=groups,dc=testcompany,dc=com\n"
|
||||||
|
+ "objectClass: groupOfNames\n"
|
||||||
|
+ "cn: admin\n"
|
||||||
|
+ "description: Administrator group\n"
|
||||||
|
+ "member: uid=admin,ou=users,dc=testcompany,dc=com";
|
||||||
|
|
||||||
|
execLdapAdd(groupsLdif, "groups");
|
||||||
|
|
||||||
|
LOG.info("✓ All test LDAP data loaded successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void execLdapAdd(String ldifContent, String description) throws Exception {
|
||||||
|
String[] ldapAddCommand = {
|
||||||
|
"/bin/bash",
|
||||||
|
"-c",
|
||||||
|
"echo '"
|
||||||
|
+ ldifContent
|
||||||
|
+ "' | ldapadd -x -H ldap://localhost:389 -D 'cn=admin,dc=testcompany,dc=com' -w adminpassword"
|
||||||
|
};
|
||||||
|
|
||||||
|
org.testcontainers.containers.Container.ExecResult result =
|
||||||
|
ldapContainer.execInContainer(ldapAddCommand);
|
||||||
|
|
||||||
|
if (result.getExitCode() != 0) {
|
||||||
|
LOG.error("Failed to add {}: {}", description, result.getStderr());
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to load " + description + ". Exit code: " + result.getExitCode());
|
||||||
|
}
|
||||||
|
LOG.info("✓ Added {}", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
void setupLdapConfiguration() throws Exception {
|
||||||
|
Thread.sleep(3000);
|
||||||
|
|
||||||
|
LOG.info("Application running on port: {}", APP.getLocalPort());
|
||||||
|
LOG.info("LDAP server running at: {}:{}", ldapHost, ldapPort);
|
||||||
|
|
||||||
|
updateAuthenticationConfigToLdap();
|
||||||
|
LOG.info("Test setup complete - LDAP authentication configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAuthenticationConfigToLdap() throws Exception {
|
||||||
|
LOG.info("Updating authentication configuration to LDAP...");
|
||||||
|
|
||||||
|
Map<String, Object> ldapConfig = new HashMap<>();
|
||||||
|
ldapConfig.put("host", ldapHost);
|
||||||
|
ldapConfig.put("port", ldapPort);
|
||||||
|
ldapConfig.put("dnAdminPrincipal", "cn=admin,dc=testcompany,dc=com");
|
||||||
|
ldapConfig.put("dnAdminPassword", "adminpassword");
|
||||||
|
ldapConfig.put("userBaseDN", "ou=users,dc=testcompany,dc=com");
|
||||||
|
ldapConfig.put("groupBaseDN", "ou=groups,dc=testcompany,dc=com");
|
||||||
|
ldapConfig.put("mailAttributeName", "mail");
|
||||||
|
ldapConfig.put("groupAttributeName", "objectClass");
|
||||||
|
ldapConfig.put("groupAttributeValue", "groupOfNames");
|
||||||
|
ldapConfig.put("groupMemberAttributeName", "member");
|
||||||
|
ldapConfig.put("allAttributeName", "*");
|
||||||
|
ldapConfig.put("roleAdminName", "admin");
|
||||||
|
ldapConfig.put("maxPoolSize", 10);
|
||||||
|
ldapConfig.put("sslEnabled", false);
|
||||||
|
|
||||||
|
Map<String, List<String>> roleMapping = new HashMap<>();
|
||||||
|
roleMapping.put("cn=admin,ou=groups,dc=testcompany,dc=com", Arrays.asList("admin"));
|
||||||
|
roleMapping.put(
|
||||||
|
"cn=DataConsumer,ou=groups,dc=testcompany,dc=com", Arrays.asList("DataConsumer"));
|
||||||
|
|
||||||
|
ldapConfig.put("authRolesMapping", new ObjectMapper().writeValueAsString(roleMapping));
|
||||||
|
ldapConfig.put("authReassignRoles", Arrays.asList("*"));
|
||||||
|
|
||||||
|
Map<String, Object> authConfig = new HashMap<>();
|
||||||
|
authConfig.put("provider", "ldap");
|
||||||
|
authConfig.put("providerName", "LDAP");
|
||||||
|
authConfig.put("publicKeyUrls", new ArrayList<>());
|
||||||
|
authConfig.put("clientId", "open-metadata");
|
||||||
|
authConfig.put("authority", "http://localhost:" + APP.getLocalPort());
|
||||||
|
authConfig.put("callbackUrl", "http://localhost:" + APP.getLocalPort() + "/callback");
|
||||||
|
authConfig.put("jwtPrincipalClaims", Arrays.asList("email", "preferred_username", "sub"));
|
||||||
|
authConfig.put("enableSelfSignup", true);
|
||||||
|
authConfig.put("ldapConfiguration", ldapConfig);
|
||||||
|
|
||||||
|
Map<String, Object> authorizerConfig = new HashMap<>();
|
||||||
|
authorizerConfig.put("className", "org.openmetadata.service.security.DefaultAuthorizer");
|
||||||
|
authorizerConfig.put("containerRequestFilter", "org.openmetadata.service.security.JwtFilter");
|
||||||
|
authorizerConfig.put("adminPrincipals", Arrays.asList("admin"));
|
||||||
|
authorizerConfig.put("testPrincipals", new ArrayList<>());
|
||||||
|
authorizerConfig.put("allowedEmailRegistrationDomains", Arrays.asList("all"));
|
||||||
|
authorizerConfig.put("principalDomain", "open-metadata.org");
|
||||||
|
authorizerConfig.put("allowedDomains", new ArrayList<>());
|
||||||
|
authorizerConfig.put("enforcePrincipalDomain", false);
|
||||||
|
authorizerConfig.put("enableSecureSocketConnection", false);
|
||||||
|
authorizerConfig.put("useRolesFromProvider", false);
|
||||||
|
|
||||||
|
Map<String, Object> securityConfig = new HashMap<>();
|
||||||
|
securityConfig.put("authenticationConfiguration", authConfig);
|
||||||
|
securityConfig.put("authorizerConfiguration", authorizerConfig);
|
||||||
|
|
||||||
|
Invocation.Builder request =
|
||||||
|
APP.client()
|
||||||
|
.target(getServerUrl() + "/api/v1/system/security/config")
|
||||||
|
.request(MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : TestUtils.ADMIN_AUTH_HEADERS.entrySet()) {
|
||||||
|
request = request.header(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
Response response = request.put(Entity.json(securityConfig));
|
||||||
|
|
||||||
|
LOG.info("Auth config update response status: {}", response.getStatus());
|
||||||
|
|
||||||
|
if (response.getStatus() == 200 || response.getStatus() == 201) {
|
||||||
|
LOG.info("✓ Successfully updated authentication configuration to LDAP");
|
||||||
|
Thread.sleep(3000);
|
||||||
|
} else {
|
||||||
|
String error = response.readEntity(String.class);
|
||||||
|
LOG.error("Failed to update authentication configuration: {}", error);
|
||||||
|
fail("Failed to update authentication configuration - Status: " + response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
void testLdapContainerStartup() {
|
||||||
|
LOG.info("Testing LDAP container startup");
|
||||||
|
assertNotNull(ldapContainer);
|
||||||
|
assertTrue(ldapContainer.isRunning());
|
||||||
|
assertNotNull(ldapHost);
|
||||||
|
assertTrue(ldapPort > 0);
|
||||||
|
LOG.info("LDAP container is running at {}:{}", ldapHost, ldapPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(3)
|
||||||
|
void testSuccessfulLoginWithValidCredentials() throws Exception {
|
||||||
|
LOG.info("Testing successful LDAP login with valid credentials");
|
||||||
|
|
||||||
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
|
loginRequest.setEmail(TEST_USER_EMAIL);
|
||||||
|
loginRequest.setPassword(Base64.getEncoder().encodeToString(TEST_USER_PASSWORD.getBytes()));
|
||||||
|
|
||||||
|
Response response =
|
||||||
|
APP.client()
|
||||||
|
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.property(ClientProperties.FOLLOW_REDIRECTS, false)
|
||||||
|
.post(Entity.json(loginRequest));
|
||||||
|
|
||||||
|
LOG.info("Login response status: {}", response.getStatus());
|
||||||
|
assertEquals(200, response.getStatus(), "Login should succeed with valid credentials");
|
||||||
|
|
||||||
|
String responseBody = response.readEntity(String.class);
|
||||||
|
LOG.info("Login response: {}", responseBody);
|
||||||
|
|
||||||
|
Map<String, Object> jwtResponse = new ObjectMapper().readValue(responseBody, Map.class);
|
||||||
|
|
||||||
|
assertNotNull(jwtResponse.get("accessToken"), "Access token should be present");
|
||||||
|
assertEquals("Bearer", jwtResponse.get("tokenType"), "Token type should be Bearer");
|
||||||
|
assertNotNull(jwtResponse.get("expiryDuration"), "Expiry duration should be present");
|
||||||
|
assertNull(jwtResponse.get("refreshToken"), "Refresh token should not be sent to client");
|
||||||
|
|
||||||
|
Map<String, NewCookie> cookies = response.getCookies();
|
||||||
|
LOG.info("Session cookies: {}", cookies.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(4)
|
||||||
|
void testFailedLoginWithInvalidPassword() throws Exception {
|
||||||
|
LOG.info("Testing failed LDAP login with invalid password");
|
||||||
|
|
||||||
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
|
loginRequest.setEmail(TEST_USER_EMAIL);
|
||||||
|
loginRequest.setPassword(Base64.getEncoder().encodeToString("wrongpassword".getBytes()));
|
||||||
|
|
||||||
|
Response response =
|
||||||
|
APP.client()
|
||||||
|
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.property(ClientProperties.FOLLOW_REDIRECTS, false)
|
||||||
|
.post(Entity.json(loginRequest));
|
||||||
|
|
||||||
|
LOG.info("Login response status: {}", response.getStatus());
|
||||||
|
assertNotEquals(200, response.getStatus(), "Login should fail with invalid password");
|
||||||
|
assertTrue(
|
||||||
|
response.getStatus() == 401 || response.getStatus() == 500,
|
||||||
|
"Should return 401 or 500 for invalid credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(5)
|
||||||
|
void testFailedLoginWithNonExistentUser() throws Exception {
|
||||||
|
LOG.info("Testing failed LDAP login with non-existent user");
|
||||||
|
|
||||||
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
|
loginRequest.setEmail("nonexistent@testcompany.com");
|
||||||
|
loginRequest.setPassword(Base64.getEncoder().encodeToString("password".getBytes()));
|
||||||
|
|
||||||
|
Response response =
|
||||||
|
APP.client()
|
||||||
|
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.property(ClientProperties.FOLLOW_REDIRECTS, false)
|
||||||
|
.post(Entity.json(loginRequest));
|
||||||
|
|
||||||
|
LOG.info("Login response status: {}", response.getStatus());
|
||||||
|
assertNotEquals(200, response.getStatus(), "Login should fail with non-existent user");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(7)
|
||||||
|
void testLogout() throws Exception {
|
||||||
|
LOG.info("Testing logout flow");
|
||||||
|
|
||||||
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
|
loginRequest.setEmail(TEST_USER_EMAIL);
|
||||||
|
loginRequest.setPassword(Base64.getEncoder().encodeToString(TEST_USER_PASSWORD.getBytes()));
|
||||||
|
|
||||||
|
Response loginResponse =
|
||||||
|
APP.client()
|
||||||
|
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.property(ClientProperties.FOLLOW_REDIRECTS, false)
|
||||||
|
.post(Entity.json(loginRequest));
|
||||||
|
|
||||||
|
assertEquals(200, loginResponse.getStatus(), "Login should succeed");
|
||||||
|
|
||||||
|
Map<String, NewCookie> cookies = loginResponse.getCookies();
|
||||||
|
|
||||||
|
Invocation.Builder logoutRequest =
|
||||||
|
APP.client()
|
||||||
|
.target(getServerUrl() + AUTH_LOGOUT_ENDPOINT)
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.property(ClientProperties.FOLLOW_REDIRECTS, false);
|
||||||
|
|
||||||
|
for (NewCookie cookie : cookies.values()) {
|
||||||
|
logoutRequest = logoutRequest.cookie(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response logoutResponse = logoutRequest.get();
|
||||||
|
|
||||||
|
LOG.info("Logout response status: {}", logoutResponse.getStatus());
|
||||||
|
assertEquals(200, logoutResponse.getStatus(), "Logout should succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(9)
|
||||||
|
void testMultipleLoginAttempts() throws Exception {
|
||||||
|
LOG.info("Testing multiple failed login attempts");
|
||||||
|
|
||||||
|
String testEmail = "multitest@testcompany.com";
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
|
loginRequest.setEmail(testEmail);
|
||||||
|
loginRequest.setPassword(Base64.getEncoder().encodeToString("wrongpassword".getBytes()));
|
||||||
|
|
||||||
|
Response response =
|
||||||
|
APP.client()
|
||||||
|
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
|
||||||
|
.request(MediaType.APPLICATION_JSON)
|
||||||
|
.property(ClientProperties.FOLLOW_REDIRECTS, false)
|
||||||
|
.post(Entity.json(loginRequest));
|
||||||
|
|
||||||
|
LOG.info("Attempt {} - Response status: {}", i + 1, response.getStatus());
|
||||||
|
assertNotEquals(200, response.getStatus(), "Login should fail with wrong password");
|
||||||
|
|
||||||
|
Thread.sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginAttemptCache.getInstance().recordSuccessfulLogin(testEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getServerUrl() {
|
||||||
|
return "http://localhost:" + APP.getLocalPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
void cleanup() {
|
||||||
|
if (ldapContainer != null && ldapContainer.isRunning()) {
|
||||||
|
LOG.info("Stopping LDAP container");
|
||||||
|
ldapContainer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user