mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-29 17:49: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; | ||||
| 
 | ||||
| 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 jakarta.servlet.http.HttpServletRequest; | ||||
| @ -16,6 +19,7 @@ import org.openmetadata.schema.auth.TokenRefreshRequest; | ||||
| import org.openmetadata.schema.utils.JsonUtils; | ||||
| import org.openmetadata.service.OpenMetadataApplicationConfig; | ||||
| import org.openmetadata.service.auth.JwtResponse; | ||||
| import org.openmetadata.service.exception.CustomExceptionMessage; | ||||
| import org.openmetadata.service.security.AuthServeletHandler; | ||||
| 
 | ||||
| @Slf4j | ||||
| @ -109,9 +113,21 @@ public class LdapAuthServletHandler implements AuthServeletHandler { | ||||
|       resp.setContentType("application/json"); | ||||
|       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) { | ||||
|       LOG.error("Error handling LDAP login", e); | ||||
|       sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); | ||||
|       LOG.error("Unexpected error handling LDAP login", e); | ||||
|       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) { | ||||
|     try { | ||||
|       resp.setStatus(status); | ||||
|       writeJsonResponse(resp, String.format("{\"error\":\"%s\"}", message)); | ||||
|       writeJsonResponse(resp, String.format("{\"message\":\"%s\"}", message)); | ||||
|     } catch (IOException 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.FORBIDDEN; | ||||
| 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 org.openmetadata.common.utils.CommonUtil.nullOrEmpty; | ||||
| import static org.openmetadata.schema.auth.TokenType.REFRESH_TOKEN; | ||||
| @ -81,6 +82,8 @@ import org.springframework.util.CollectionUtils; | ||||
| @Slf4j | ||||
| public class LdapAuthenticator implements AuthenticatorHandler { | ||||
|   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 UserRepository userRepository; | ||||
|   private TokenRepository tokenRepository; | ||||
| @ -197,29 +200,74 @@ public class LdapAuthenticator implements AuthenticatorHandler { | ||||
|   @Override | ||||
|   public void validatePassword(String userDn, String reqPassword, User dummy) | ||||
|       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; | ||||
|     LDAPConnection userConnection = null; | ||||
|     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())) { | ||||
|         return; | ||||
|       } | ||||
|     } catch (Exception ex) { | ||||
|       if (bindingResult != null | ||||
|           && Objects.equals( | ||||
|               bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) { | ||||
| 
 | ||||
|       if (Objects.equals( | ||||
|           bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) { | ||||
|         recordFailedLoginAttempt(dummy.getEmail(), dummy.getName()); | ||||
|         throw new CustomExceptionMessage( | ||||
|             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( | ||||
|           INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, bindingResult.getResultCode().getName()); | ||||
|     } else { | ||||
|       throw new CustomExceptionMessage( | ||||
|           INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD); | ||||
|     } | ||||
| 
 | ||||
|     LOG.error( | ||||
|         "LDAP bind failed with unexpected result: {}", bindingResult.getResultCode().getName()); | ||||
|     throw new CustomExceptionMessage( | ||||
|         INTERNAL_SERVER_ERROR, | ||||
|         "LDAP_AUTH_ERROR", | ||||
|         "Authentication failed: " + bindingResult.getResultCode().getName()); | ||||
|   } | ||||
| 
 | ||||
|   @Override | ||||
| @ -237,6 +285,24 @@ public class LdapAuthenticator implements AuthenticatorHandler { | ||||
|   } | ||||
| 
 | ||||
|   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 { | ||||
|       Filter emailFilter = | ||||
|           Filter.createEqualityFilter(ldapConfiguration.getMailAttributeName(), email); | ||||
| @ -270,7 +336,26 @@ public class LdapAuthenticator implements AuthenticatorHandler { | ||||
|             INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD); | ||||
|       } | ||||
|     } 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; | ||||
|   } | ||||
| 
 | ||||
|   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
	 Ajith Prasad
						Ajith Prasad