[FIX] ldap User Name (#17764)

* migrate userName to emailPrefix for ldap auth provider.

* migrate userName to emailPrefix for ldap auth provider.

* fix migration.

* move migration to v155

* Update Ldap Authenticator

* Fix Ldap Issue on Login

---------

Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com>
Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com>
This commit is contained in:
Siddhant 2024-09-23 11:35:20 +05:30 committed by GitHub
parent c1e20873b1
commit bc3d4c778e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 226 additions and 63 deletions

View File

@ -454,6 +454,7 @@ public class OpenMetadataApplication extends Application<OpenMetadataApplication
connectionType, connectionType,
conf.getMigrationConfiguration().getExtensionPath(), conf.getMigrationConfiguration().getExtensionPath(),
conf.getPipelineServiceClientConfiguration(), conf.getPipelineServiceClientConfiguration(),
conf.getAuthenticationConfiguration(),
false); false);
migrationWorkflow.loadMigrations(); migrationWorkflow.loadMigrations();
migrationWorkflow.validateMigrationsForServer(); migrationWorkflow.validateMigrationsForServer();

View File

@ -9,6 +9,7 @@ import java.util.Map;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.core.Handle; import org.jdbi.v3.core.Handle;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.sdk.PipelineServiceClientInterface; import org.openmetadata.sdk.PipelineServiceClientInterface;
import org.openmetadata.service.clients.pipeline.PipelineServiceClientFactory; import org.openmetadata.service.clients.pipeline.PipelineServiceClientFactory;
import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jdbi3.CollectionDAO;
@ -24,6 +25,7 @@ public class MigrationProcessImpl implements MigrationProcess {
protected CollectionDAO collectionDAO; protected CollectionDAO collectionDAO;
protected Handle handle; protected Handle handle;
protected PipelineServiceClientInterface pipelineServiceClient; protected PipelineServiceClientInterface pipelineServiceClient;
protected AuthenticationConfiguration authenticationConfiguration;
private final MigrationFile migrationFile; private final MigrationFile migrationFile;
public @Getter MigrationContext context; public @Getter MigrationContext context;
@ -40,6 +42,7 @@ public class MigrationProcessImpl implements MigrationProcess {
this.pipelineServiceClient = this.pipelineServiceClient =
PipelineServiceClientFactory.createPipelineServiceClient( PipelineServiceClientFactory.createPipelineServiceClient(
this.migrationFile.pipelineServiceClientConfiguration); this.migrationFile.pipelineServiceClientConfiguration);
this.authenticationConfiguration = migrationFile.authenticationConfiguration;
} }
@Override @Override

View File

@ -18,6 +18,7 @@ import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.Jdbi; import org.jdbi.v3.core.Jdbi;
import org.json.JSONObject; import org.json.JSONObject;
import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration; import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.service.jdbi3.MigrationDAO; import org.openmetadata.service.jdbi3.MigrationDAO;
import org.openmetadata.service.jdbi3.locator.ConnectionType; import org.openmetadata.service.jdbi3.locator.ConnectionType;
import org.openmetadata.service.migration.QueryStatus; import org.openmetadata.service.migration.QueryStatus;
@ -35,6 +36,7 @@ public class MigrationWorkflow {
private final ConnectionType connectionType; private final ConnectionType connectionType;
private final String extensionSQLScriptRootPath; private final String extensionSQLScriptRootPath;
@Getter private final PipelineServiceClientConfiguration pipelineServiceClientConfiguration; @Getter private final PipelineServiceClientConfiguration pipelineServiceClientConfiguration;
@Getter private final AuthenticationConfiguration authenticationConfiguration;
private final MigrationDAO migrationDAO; private final MigrationDAO migrationDAO;
private final Jdbi jdbi; private final Jdbi jdbi;
private final boolean forceMigrations; private final boolean forceMigrations;
@ -47,6 +49,7 @@ public class MigrationWorkflow {
ConnectionType connectionType, ConnectionType connectionType,
String extensionSQLScriptRootPath, String extensionSQLScriptRootPath,
PipelineServiceClientConfiguration pipelineServiceClientConfiguration, PipelineServiceClientConfiguration pipelineServiceClientConfiguration,
AuthenticationConfiguration authenticationConfiguration,
boolean forceMigrations) { boolean forceMigrations) {
this.jdbi = jdbi; this.jdbi = jdbi;
this.migrationDAO = jdbi.onDemand(MigrationDAO.class); this.migrationDAO = jdbi.onDemand(MigrationDAO.class);
@ -55,6 +58,7 @@ public class MigrationWorkflow {
this.connectionType = connectionType; this.connectionType = connectionType;
this.extensionSQLScriptRootPath = extensionSQLScriptRootPath; this.extensionSQLScriptRootPath = extensionSQLScriptRootPath;
this.pipelineServiceClientConfiguration = pipelineServiceClientConfiguration; this.pipelineServiceClientConfiguration = pipelineServiceClientConfiguration;
this.authenticationConfiguration = authenticationConfiguration;
} }
public void loadMigrations() { public void loadMigrations() {
@ -64,7 +68,8 @@ public class MigrationWorkflow {
nativeSQLScriptRootPath, nativeSQLScriptRootPath,
connectionType, connectionType,
extensionSQLScriptRootPath, extensionSQLScriptRootPath,
pipelineServiceClientConfiguration); pipelineServiceClientConfiguration,
authenticationConfiguration);
// Filter Migrations to Be Run // Filter Migrations to Be Run
this.migrations = filterAndGetMigrationsToRun(availableMigrations); this.migrations = filterAndGetMigrationsToRun(availableMigrations);
} }
@ -83,10 +88,15 @@ public class MigrationWorkflow {
String nativeSQLScriptRootPath, String nativeSQLScriptRootPath,
ConnectionType connectionType, ConnectionType connectionType,
String extensionSQLScriptRootPath, String extensionSQLScriptRootPath,
PipelineServiceClientConfiguration pipelineServiceClientConfiguration) { PipelineServiceClientConfiguration pipelineServiceClientConfiguration,
AuthenticationConfiguration authenticationConfiguration) {
List<MigrationFile> availableOMNativeMigrations = List<MigrationFile> availableOMNativeMigrations =
getMigrationFilesFromPath( getMigrationFilesFromPath(
nativeSQLScriptRootPath, connectionType, pipelineServiceClientConfiguration, false); nativeSQLScriptRootPath,
connectionType,
pipelineServiceClientConfiguration,
authenticationConfiguration,
false);
// If we only have OM migrations, return them // If we only have OM migrations, return them
if (extensionSQLScriptRootPath == null || extensionSQLScriptRootPath.isEmpty()) { if (extensionSQLScriptRootPath == null || extensionSQLScriptRootPath.isEmpty()) {
@ -96,7 +106,11 @@ public class MigrationWorkflow {
// Otherwise, fetch the extension migrations and sort the executions // Otherwise, fetch the extension migrations and sort the executions
List<MigrationFile> availableExtensionMigrations = List<MigrationFile> availableExtensionMigrations =
getMigrationFilesFromPath( getMigrationFilesFromPath(
extensionSQLScriptRootPath, connectionType, pipelineServiceClientConfiguration, true); extensionSQLScriptRootPath,
connectionType,
pipelineServiceClientConfiguration,
authenticationConfiguration,
true);
/* /*
If we create migrations version as: If we create migrations version as:
@ -114,6 +128,7 @@ public class MigrationWorkflow {
String path, String path,
ConnectionType connectionType, ConnectionType connectionType,
PipelineServiceClientConfiguration pipelineServiceClientConfiguration, PipelineServiceClientConfiguration pipelineServiceClientConfiguration,
AuthenticationConfiguration authenticationConfiguration,
Boolean isExtension) { Boolean isExtension) {
return Arrays.stream(Objects.requireNonNull(new File(path).listFiles(File::isDirectory))) return Arrays.stream(Objects.requireNonNull(new File(path).listFiles(File::isDirectory)))
.map( .map(
@ -123,6 +138,7 @@ public class MigrationWorkflow {
migrationDAO, migrationDAO,
connectionType, connectionType,
pipelineServiceClientConfiguration, pipelineServiceClientConfiguration,
authenticationConfiguration,
isExtension)) isExtension))
.sorted() .sorted()
.toList(); .toList();

View File

@ -0,0 +1,21 @@
package org.openmetadata.service.migration.mysql.v155;
import static org.openmetadata.service.migration.utils.v155.MigrationUtil.updateUserNameToEmailPrefixForLdapAuthProvider;
import lombok.SneakyThrows;
import org.openmetadata.service.migration.api.MigrationProcessImpl;
import org.openmetadata.service.migration.utils.MigrationFile;
public class Migration extends MigrationProcessImpl {
public Migration(MigrationFile migrationFile) {
super(migrationFile);
}
@Override
@SneakyThrows
public void runDataMigration() {
updateUserNameToEmailPrefixForLdapAuthProvider(
handle, collectionDAO, authenticationConfiguration, false);
}
}

View File

@ -0,0 +1,21 @@
package org.openmetadata.service.migration.postgres.v155;
import static org.openmetadata.service.migration.utils.v155.MigrationUtil.updateUserNameToEmailPrefixForLdapAuthProvider;
import lombok.SneakyThrows;
import org.openmetadata.service.migration.api.MigrationProcessImpl;
import org.openmetadata.service.migration.utils.MigrationFile;
public class Migration extends MigrationProcessImpl {
public Migration(MigrationFile migrationFile) {
super(migrationFile);
}
@Override
@SneakyThrows
public void runDataMigration() {
updateUserNameToEmailPrefixForLdapAuthProvider(
handle, collectionDAO, authenticationConfiguration, true);
}
}

View File

@ -14,6 +14,7 @@ import org.flywaydb.core.internal.resource.filesystem.FileSystemResource;
import org.flywaydb.core.internal.sqlscript.SqlStatementIterator; import org.flywaydb.core.internal.sqlscript.SqlStatementIterator;
import org.flywaydb.database.mysql.MySQLParser; import org.flywaydb.database.mysql.MySQLParser;
import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration; import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.service.jdbi3.MigrationDAO; import org.openmetadata.service.jdbi3.MigrationDAO;
import org.openmetadata.service.jdbi3.locator.ConnectionType; import org.openmetadata.service.jdbi3.locator.ConnectionType;
import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil;
@ -23,6 +24,8 @@ public class MigrationFile implements Comparable<MigrationFile> {
public final String version; public final String version;
public final ConnectionType connectionType; public final ConnectionType connectionType;
public final PipelineServiceClientConfiguration pipelineServiceClientConfiguration; public final PipelineServiceClientConfiguration pipelineServiceClientConfiguration;
public final AuthenticationConfiguration authenticationConfiguration;
public final File dir; public final File dir;
public final Boolean isExtension; public final Boolean isExtension;
public final String dbPackageName; public final String dbPackageName;
@ -38,6 +41,7 @@ public class MigrationFile implements Comparable<MigrationFile> {
MigrationDAO migrationDAO, MigrationDAO migrationDAO,
ConnectionType connectionType, ConnectionType connectionType,
PipelineServiceClientConfiguration pipelineServiceClientConfiguration, PipelineServiceClientConfiguration pipelineServiceClientConfiguration,
AuthenticationConfiguration authenticationConfiguration,
Boolean isExtension) { Boolean isExtension) {
this.dir = dir; this.dir = dir;
this.isExtension = isExtension; this.isExtension = isExtension;
@ -45,6 +49,7 @@ public class MigrationFile implements Comparable<MigrationFile> {
this.connectionType = connectionType; this.connectionType = connectionType;
this.migrationDAO = migrationDAO; this.migrationDAO = migrationDAO;
this.pipelineServiceClientConfiguration = pipelineServiceClientConfiguration; this.pipelineServiceClientConfiguration = pipelineServiceClientConfiguration;
this.authenticationConfiguration = authenticationConfiguration;
this.dbPackageName = connectionType == ConnectionType.MYSQL ? "mysql" : "postgres"; this.dbPackageName = connectionType == ConnectionType.MYSQL ? "mysql" : "postgres";
versionNumbers = convertToNumber(version); versionNumbers = convertToNumber(version);
schemaChanges = new ArrayList<>(); schemaChanges = new ArrayList<>();

View File

@ -0,0 +1,60 @@
package org.openmetadata.service.migration.utils.v155;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.core.Handle;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.services.connections.metadata.AuthProvider;
import org.openmetadata.schema.utils.EntityInterfaceUtil;
import org.openmetadata.service.jdbi3.CollectionDAO;
import org.openmetadata.service.util.JsonUtils;
@Slf4j
public class MigrationUtil {
public static void updateUserNameToEmailPrefixForLdapAuthProvider(
Handle handle,
CollectionDAO daoCollection,
AuthenticationConfiguration config,
boolean postgres) {
if (config.getProvider().equals(AuthProvider.LDAP)) {
LOG.info("Starting migration username -> email prefix");
int total = daoCollection.userDAO().listTotalCount();
int offset = 0;
int limit = 200;
while (offset < total) {
List<String> userEntities = daoCollection.userDAO().listAfterWithOffset(limit, offset);
for (String json : userEntities) {
User userEntity = JsonUtils.readValue(json, User.class);
String email = userEntity.getEmail();
String emailPrefix = email.substring(0, email.indexOf("@"));
userEntity.setFullyQualifiedName(EntityInterfaceUtil.quoteName(emailPrefix));
userEntity.setName(emailPrefix);
daoCollection.userDAO().update(userEntity);
}
offset = offset + limit;
}
updateUserEntityNameHash(handle, postgres);
LOG.info("Completed migrating username -> email prefix");
}
}
public static void updateUserEntityNameHash(Handle handle, boolean postgres) {
String updateNameHashSql;
if (postgres) {
updateNameHashSql = "UPDATE user_entity SET nameHash = MD5(json ->> 'fullyQualifiedName');";
} else {
updateNameHashSql =
"UPDATE user_entity SET nameHash = MD5(JSON_UNQUOTE(JSON_EXTRACT(json, '$.fullyQualifiedName')));";
}
try {
handle.execute(updateNameHashSql);
LOG.info("Successfully updated nameHash for all user entities.");
} catch (Exception e) {
LOG.error("Error updating nameHash field", e);
}
}
}

View File

@ -33,13 +33,13 @@ public interface AuthenticatorHandler {
void checkIfLoginBlocked(String userName); void checkIfLoginBlocked(String userName);
void recordFailedLoginAttempt(String providedIdentity, User user) void recordFailedLoginAttempt(String email, String userName)
throws TemplateException, IOException; throws TemplateException, IOException;
void validatePassword(String providedIdentity, User storedUser, String reqPassword) void validatePassword(String providedIdentity, String reqPassword, User omUser)
throws TemplateException, IOException; throws TemplateException, IOException;
User lookUserInProvider(String userName); User lookUserInProvider(String email, String pwd) throws TemplateException, IOException;
default User registerUser(RegistrationRequest registrationRequest) { default User registerUser(RegistrationRequest registrationRequest) {
throw new CustomExceptionMessage( throw new CustomExceptionMessage(

View File

@ -261,7 +261,8 @@ public class BasicAuthenticator implements AuthenticatorHandler {
// Update user about Password Change // Update user about Password Change
try { try {
sendAccountStatus(storedUser, "Update Password", "Change Successful"); sendAccountStatus(
storedUser.getName(), storedUser.getEmail(), "Update Password", "Change Successful");
} catch (TemplateException ex) { } catch (TemplateException ex) {
LOG.error("Error in sending Password Change Mail to User. Reason : " + ex.getMessage(), ex); LOG.error("Error in sending Password Change Mail to User. Reason : " + ex.getMessage(), ex);
throw new CustomExceptionMessage(424, FAILED_SEND_EMAIL, EMAIL_SENDING_ISSUE); throw new CustomExceptionMessage(424, FAILED_SEND_EMAIL, EMAIL_SENDING_ISSUE);
@ -466,28 +467,29 @@ public class BasicAuthenticator implements AuthenticatorHandler {
@Override @Override
public JwtResponse loginUser(LoginRequest loginRequest) throws IOException, TemplateException { public JwtResponse loginUser(LoginRequest loginRequest) throws IOException, TemplateException {
String userName = loginRequest.getEmail(); String email = loginRequest.getEmail();
checkIfLoginBlocked(userName); checkIfLoginBlocked(email);
User storedUser = lookUserInProvider(userName); User storedUser = lookUserInProvider(email, loginRequest.getPassword());
validatePassword(userName, storedUser, loginRequest.getPassword()); validatePassword(email, loginRequest.getPassword(), storedUser);
return getJwtResponse(storedUser, SecurityUtil.getLoginConfiguration().getJwtTokenExpiryTime()); return getJwtResponse(storedUser, SecurityUtil.getLoginConfiguration().getJwtTokenExpiryTime());
} }
@Override @Override
public void checkIfLoginBlocked(String userName) { public void checkIfLoginBlocked(String email) {
if (loginAttemptCache.isLoginBlocked(userName)) { if (loginAttemptCache.isLoginBlocked(email)) {
throw new AuthenticationException(MAX_FAILED_LOGIN_ATTEMPT); throw new AuthenticationException(MAX_FAILED_LOGIN_ATTEMPT);
} }
} }
@Override @Override
public void recordFailedLoginAttempt(String providedIdentity, User storedUser) public void recordFailedLoginAttempt(String email, String userName)
throws TemplateException, IOException { throws TemplateException, IOException {
loginAttemptCache.recordFailedLogin(providedIdentity); loginAttemptCache.recordFailedLogin(email);
int failedLoginAttempt = loginAttemptCache.getUserFailedLoginCount(providedIdentity); int failedLoginAttempt = loginAttemptCache.getUserFailedLoginCount(email);
if (failedLoginAttempt == SecurityUtil.getLoginConfiguration().getMaxLoginFailAttempts()) { if (failedLoginAttempt == SecurityUtil.getLoginConfiguration().getMaxLoginFailAttempts()) {
sendAccountStatus( sendAccountStatus(
storedUser, userName,
email,
"Multiple Failed Login Attempts.", "Multiple Failed Login Attempts.",
String.format( String.format(
"Someone is trying to access your account. Login is Blocked for %s minutes. Please change your password.", "Someone is trying to access your account. Login is Blocked for %s minutes. Please change your password.",
@ -495,35 +497,35 @@ public class BasicAuthenticator implements AuthenticatorHandler {
} }
} }
public void validatePassword(String providedIdentity, User storedUser, String reqPassword) public void validatePassword(String providedIdentity, String reqPassword, User omUser)
throws TemplateException, IOException { throws TemplateException, IOException {
// when basic auth is enabled and the user is created through the API without password, the // when basic auth is enabled and the user is created through the API without password, the
// stored auth mechanism // stored auth mechanism
// for the user is null // for the user is null
if (storedUser.getAuthenticationMechanism() == null) { if (omUser.getAuthenticationMechanism() == null) {
throw new AuthenticationException(INVALID_USERNAME_PASSWORD); throw new AuthenticationException(INVALID_USERNAME_PASSWORD);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
LinkedHashMap<String, String> storedData = LinkedHashMap<String, String> storedData =
(LinkedHashMap<String, String>) storedUser.getAuthenticationMechanism().getConfig(); (LinkedHashMap<String, String>) omUser.getAuthenticationMechanism().getConfig();
String storedHashPassword = storedData.get("password"); String storedHashPassword = storedData.get("password");
if (!BCrypt.verifyer().verify(reqPassword.toCharArray(), storedHashPassword).verified) { if (!BCrypt.verifyer().verify(reqPassword.toCharArray(), storedHashPassword).verified) {
// record Failed Login Attempts // record Failed Login Attempts
recordFailedLoginAttempt(providedIdentity, storedUser); recordFailedLoginAttempt(omUser.getEmail(), omUser.getName());
throw new AuthenticationException(INVALID_USERNAME_PASSWORD); throw new AuthenticationException(INVALID_USERNAME_PASSWORD);
} }
} }
@Override @Override
public User lookUserInProvider(String userName) { public User lookUserInProvider(String email, String pwd) {
User storedUser = null; User storedUser = null;
try { try {
if (userName.contains("@")) { if (email.contains("@")) {
// lookup by User Email // lookup by User Email
storedUser = storedUser =
userRepository.getByEmail( userRepository.getByEmail(
null, null,
userName, email,
new EntityUtil.Fields( new EntityUtil.Fields(
Set.of(USER_PROTECTED_FIELDS, "roles"), "authenticationMechanism,roles")); Set.of(USER_PROTECTED_FIELDS, "roles"), "authenticationMechanism,roles"));
} }

View File

@ -12,6 +12,8 @@ import static org.openmetadata.service.exception.CatalogExceptionMessage.LDAP_MI
import static org.openmetadata.service.exception.CatalogExceptionMessage.MAX_FAILED_LOGIN_ATTEMPT; import static org.openmetadata.service.exception.CatalogExceptionMessage.MAX_FAILED_LOGIN_ATTEMPT;
import static org.openmetadata.service.exception.CatalogExceptionMessage.MULTIPLE_EMAIL_ENTRIES; import static org.openmetadata.service.exception.CatalogExceptionMessage.MULTIPLE_EMAIL_ENTRIES;
import static org.openmetadata.service.exception.CatalogExceptionMessage.PASSWORD_RESET_TOKEN_EXPIRED; import static org.openmetadata.service.exception.CatalogExceptionMessage.PASSWORD_RESET_TOKEN_EXPIRED;
import static org.openmetadata.service.exception.CatalogExceptionMessage.SELF_SIGNUP_DISABLED_MESSAGE;
import static org.openmetadata.service.exception.CatalogExceptionMessage.SELF_SIGNUP_NOT_ENABLED;
import static org.openmetadata.service.util.UserUtil.getRoleListFromUser; import static org.openmetadata.service.util.UserUtil.getRoleListFromUser;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
@ -85,6 +87,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
private LoginAttemptCache loginAttemptCache; private LoginAttemptCache loginAttemptCache;
private LdapConfiguration ldapConfiguration; private LdapConfiguration ldapConfiguration;
private LDAPConnectionPool ldapLookupConnectionPool; private LDAPConnectionPool ldapLookupConnectionPool;
private boolean isSelfSignUpEnabled;
@Override @Override
public void init(OpenMetadataApplicationConfig config) { public void init(OpenMetadataApplicationConfig config) {
@ -100,6 +103,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
this.tokenRepository = Entity.getTokenRepository(); this.tokenRepository = Entity.getTokenRepository();
this.ldapConfiguration = config.getAuthenticationConfiguration().getLdapConfiguration(); this.ldapConfiguration = config.getAuthenticationConfiguration().getLdapConfiguration();
this.loginAttemptCache = new LoginAttemptCache(); this.loginAttemptCache = new LoginAttemptCache();
this.isSelfSignUpEnabled = config.getAuthenticationConfiguration().getEnableSelfSignup();
} }
private LDAPConnectionPool getLdapConnectionPool(LdapConfiguration ldapConfiguration) { private LDAPConnectionPool getLdapConnectionPool(LdapConfiguration ldapConfiguration) {
@ -138,10 +142,9 @@ public class LdapAuthenticator implements AuthenticatorHandler {
@Override @Override
public JwtResponse loginUser(LoginRequest loginRequest) throws IOException, TemplateException { public JwtResponse loginUser(LoginRequest loginRequest) throws IOException, TemplateException {
checkIfLoginBlocked(loginRequest.getEmail()); String email = loginRequest.getEmail();
User storedUser = lookUserInProvider(loginRequest.getEmail()); checkIfLoginBlocked(email);
validatePassword(storedUser.getEmail(), storedUser, loginRequest.getPassword()); User omUser = lookUserInProvider(email, loginRequest.getPassword());
User omUser = checkAndCreateUser(storedUser.getEmail(), storedUser.getName());
return getJwtResponse(omUser, SecurityUtil.getLoginConfiguration().getJwtTokenExpiryTime()); return getJwtResponse(omUser, SecurityUtil.getLoginConfiguration().getJwtTokenExpiryTime());
} }
@ -149,21 +152,25 @@ public class LdapAuthenticator implements AuthenticatorHandler {
* Check if the user exists in database by userName, if user exist, reassign roles for user according to it's ldap * Check if the user exists in database by userName, if user exist, reassign roles for user according to it's ldap
* group else, create a new user and assign roles according to it's ldap group * group else, create a new user and assign roles according to it's ldap group
* *
* @param email email address of user * @param userDn userDn from LDAP
* @param name userName of user * @param email Email of the User
* @return user info * @return user info
* @author Eric Wen@2023-07-16 17:06:43 * @author Eric Wen@2023-07-16 17:06:43
*/ */
private User checkAndCreateUser(String email, String name) throws IOException { private User checkAndCreateUser(String userDn, String email, String userName) throws IOException {
// Check if the user exists in OM Database // Check if the user exists in OM Database
try { try {
User omUser = User omUser =
userRepository.getByName(null, name, userRepository.getFields("id,name,email,roles")); userRepository.getByEmail(null, email, userRepository.getFields("id,name,email,roles"));
getRoleForLdap(omUser, Boolean.TRUE); getRoleForLdap(userDn, omUser, Boolean.TRUE);
return omUser; return omUser;
} catch (EntityNotFoundException ex) { } catch (EntityNotFoundException ex) {
// User does not exist if (isSelfSignUpEnabled) {
return userRepository.create(null, getUserForLdap(email, name)); return userRepository.create(null, getUserForLdap(userDn, email, userName));
} else {
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR, SELF_SIGNUP_NOT_ENABLED, SELF_SIGNUP_DISABLED_MESSAGE);
}
} }
} }
@ -175,13 +182,14 @@ public class LdapAuthenticator implements AuthenticatorHandler {
} }
@Override @Override
public void recordFailedLoginAttempt(String providedIdentity, User storedUser) public void recordFailedLoginAttempt(String email, String userName)
throws TemplateException, IOException { throws TemplateException, IOException {
loginAttemptCache.recordFailedLogin(providedIdentity); loginAttemptCache.recordFailedLogin(email);
int failedLoginAttempt = loginAttemptCache.getUserFailedLoginCount(providedIdentity); int failedLoginAttempt = loginAttemptCache.getUserFailedLoginCount(email);
if (failedLoginAttempt == SecurityUtil.getLoginConfiguration().getMaxLoginFailAttempts()) { if (failedLoginAttempt == SecurityUtil.getLoginConfiguration().getMaxLoginFailAttempts()) {
EmailUtil.sendAccountStatus( EmailUtil.sendAccountStatus(
storedUser, userName,
email,
"Multiple Failed Login Attempts.", "Multiple Failed Login Attempts.",
String.format( String.format(
"Someone is tried accessing your account. Login is Blocked for %s seconds.", "Someone is tried accessing your account. Login is Blocked for %s seconds.",
@ -190,12 +198,12 @@ public class LdapAuthenticator implements AuthenticatorHandler {
} }
@Override @Override
public void validatePassword(String providedIdentity, User storedUser, String reqPassword) 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 // performed in LDAP , the storedUser's name set as DN of the User in Ldap
BindResult bindingResult = null; BindResult bindingResult = null;
try { try {
bindingResult = ldapLookupConnectionPool.bind(storedUser.getName(), reqPassword); bindingResult = ldapLookupConnectionPool.bind(userDn, reqPassword);
if (Objects.equals(bindingResult.getResultCode().getName(), ResultCode.SUCCESS.getName())) { if (Objects.equals(bindingResult.getResultCode().getName(), ResultCode.SUCCESS.getName())) {
return; return;
} }
@ -203,7 +211,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
if (bindingResult != null if (bindingResult != null
&& Objects.equals( && Objects.equals(
bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) { bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) {
recordFailedLoginAttempt(providedIdentity, storedUser); 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);
} }
@ -218,7 +226,20 @@ public class LdapAuthenticator implements AuthenticatorHandler {
} }
@Override @Override
public User lookUserInProvider(String email) { public User lookUserInProvider(String email, String pwd) throws TemplateException, IOException {
String userDN = getUserDnFromLdap(email);
if (!nullOrEmpty(userDN)) {
User dummy = getUserForLdap(email);
validatePassword(userDN, pwd, dummy);
return checkAndCreateUser(userDN, email, dummy.getName());
}
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
}
private String getUserDnFromLdap(String email) {
try { try {
Filter emailFilter = Filter emailFilter =
Filter.createEqualityFilter(ldapConfiguration.getMailAttributeName(), email); Filter.createEqualityFilter(ldapConfiguration.getMailAttributeName(), email);
@ -237,8 +258,10 @@ public class LdapAuthenticator implements AuthenticatorHandler {
Attribute emailAttr = Attribute emailAttr =
searchResultEntry.getAttribute(ldapConfiguration.getMailAttributeName()); searchResultEntry.getAttribute(ldapConfiguration.getMailAttributeName());
if (!CommonUtil.nullOrEmpty(userDN) && emailAttr != null) { if (!CommonUtil.nullOrEmpty(userDN)
return getUserForLdap(email).withName(userDN.toLowerCase()); && emailAttr != null
&& email.equalsIgnoreCase(emailAttr.getValue())) {
return userDN;
} else { } else {
throw new CustomExceptionMessage(FORBIDDEN, INVALID_USER_OR_PASSWORD, LDAP_MISSING_ATTR); throw new CustomExceptionMessage(FORBIDDEN, INVALID_USER_OR_PASSWORD, LDAP_MISSING_ATTR);
} }
@ -247,7 +270,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
INTERNAL_SERVER_ERROR, MULTIPLE_EMAIL_ENTRIES, MULTIPLE_EMAIL_ENTRIES); INTERNAL_SERVER_ERROR, MULTIPLE_EMAIL_ENTRIES, MULTIPLE_EMAIL_ENTRIES);
} else { } else {
throw new CustomExceptionMessage( throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR, MULTIPLE_EMAIL_ENTRIES, 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()); throw new CustomExceptionMessage(INTERNAL_SERVER_ERROR, "LDAP_ERROR", ex.getMessage());
@ -262,14 +285,14 @@ public class LdapAuthenticator implements AuthenticatorHandler {
.withAuthenticationMechanism(null); .withAuthenticationMechanism(null);
} }
private User getUserForLdap(String email, String userName) { private User getUserForLdap(String ldapUserDn, String email, String userName) {
User user = User user =
UserUtil.getUser( UserUtil.getUser(
userName, new CreateUser().withName(userName).withEmail(email).withIsBot(false)) userName, new CreateUser().withName(userName).withEmail(email).withIsBot(false))
.withIsEmailVerified(false) .withIsEmailVerified(false)
.withAuthenticationMechanism(null); .withAuthenticationMechanism(null);
try { try {
getRoleForLdap(user, false); getRoleForLdap(ldapUserDn, user, false);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
LOG.error( LOG.error(
"Failed to assign roles from LDAP to OpenMetadata for the user {} due to {}", "Failed to assign roles from LDAP to OpenMetadata for the user {} due to {}",
@ -286,7 +309,8 @@ public class LdapAuthenticator implements AuthenticatorHandler {
* @param reAssign flag to decide whether to reassign roles * @param reAssign flag to decide whether to reassign roles
* @author Eric Wen@2023-07-16 17:23:57 * @author Eric Wen@2023-07-16 17:23:57
*/ */
private void getRoleForLdap(User user, Boolean reAssign) throws JsonProcessingException { private void getRoleForLdap(String userDn, User user, Boolean reAssign)
throws JsonProcessingException {
// Get user's groups from LDAP server using the DN of the user // Get user's groups from LDAP server using the DN of the user
try { try {
Filter groupFilter = Filter groupFilter =
@ -294,8 +318,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
ldapConfiguration.getGroupAttributeName(), ldapConfiguration.getGroupAttributeName(),
ldapConfiguration.getGroupAttributeValue()); ldapConfiguration.getGroupAttributeValue());
Filter groupMemberAttr = Filter groupMemberAttr =
Filter.createEqualityFilter( Filter.createEqualityFilter(ldapConfiguration.getGroupMemberAttributeName(), userDn);
ldapConfiguration.getGroupMemberAttributeName(), user.getName());
Filter groupAndMemberFilter = Filter.createANDFilter(groupFilter, groupMemberAttr); Filter groupAndMemberFilter = Filter.createANDFilter(groupFilter, groupMemberAttr);
SearchRequest searchRequest = SearchRequest searchRequest =
new SearchRequest( new SearchRequest(

View File

@ -33,7 +33,7 @@ public class NoopAuthenticator implements AuthenticatorHandler {
} }
@Override @Override
public void recordFailedLoginAttempt(String providedIdentity, User user) { public void recordFailedLoginAttempt(String providedIdentity, String userName) {
throw new CustomExceptionMessage( throw new CustomExceptionMessage(
Response.Status.FORBIDDEN, Response.Status.FORBIDDEN,
AUTHENTICATOR_OPERATION_NOT_SUPPORTED, AUTHENTICATOR_OPERATION_NOT_SUPPORTED,
@ -41,7 +41,7 @@ public class NoopAuthenticator implements AuthenticatorHandler {
} }
@Override @Override
public void validatePassword(String providedIdentity, User storedUser, String reqPassword) { public void validatePassword(String providedIdentity, String reqPassword, User storedUser) {
throw new CustomExceptionMessage( throw new CustomExceptionMessage(
Response.Status.FORBIDDEN, Response.Status.FORBIDDEN,
AUTHENTICATOR_OPERATION_NOT_SUPPORTED, AUTHENTICATOR_OPERATION_NOT_SUPPORTED,
@ -49,7 +49,7 @@ public class NoopAuthenticator implements AuthenticatorHandler {
} }
@Override @Override
public User lookUserInProvider(String userName) { public User lookUserInProvider(String email, String pwd) {
throw new CustomExceptionMessage( throw new CustomExceptionMessage(
Response.Status.FORBIDDEN, Response.Status.FORBIDDEN,
AUTHENTICATOR_OPERATION_NOT_SUPPORTED, AUTHENTICATOR_OPERATION_NOT_SUPPORTED,

View File

@ -618,6 +618,7 @@ public class OpenMetadataOperations implements Callable<Integer> {
connType, connType,
extensionSQLScriptRootPath, extensionSQLScriptRootPath,
config.getPipelineServiceClientConfiguration(), config.getPipelineServiceClientConfiguration(),
config.getAuthenticationConfiguration(),
force); force);
workflow.loadMigrations(); workflow.loadMigrations();
workflow.printMigrationInfo(); workflow.printMigrationInfo();

View File

@ -121,7 +121,7 @@ public class EmailUtil {
return null; return null;
} }
public static void sendAccountStatus(User user, String action, String status) public static void sendAccountStatus(String userName, String email, String action, String status)
throws IOException, TemplateException { throws IOException, TemplateException {
if (Boolean.TRUE.equals(getSmtpSettings().getEnableSmtpServer())) { if (Boolean.TRUE.equals(getSmtpSettings().getEnableSmtpServer())) {
@ -129,7 +129,7 @@ public class EmailUtil {
new TemplatePopulatorBuilder() new TemplatePopulatorBuilder()
.add(ENTITY, getSmtpSettings().getEmailingEntity()) .add(ENTITY, getSmtpSettings().getEmailingEntity())
.add(SUPPORT_URL, getSmtpSettings().getSupportUrl()) .add(SUPPORT_URL, getSmtpSettings().getSupportUrl())
.add(USERNAME, user.getName()) .add(USERNAME, userName)
.add(ACTION_KEY, action) .add(ACTION_KEY, action)
.add(ACTION_STATUS_KEY, status) .add(ACTION_STATUS_KEY, status)
.build(); .build();
@ -137,11 +137,11 @@ public class EmailUtil {
sendMail( sendMail(
getAccountStatusChangeSubject(), getAccountStatusChangeSubject(),
templatePopulator, templatePopulator,
user.getEmail(), email,
ACCOUNT_ACTIVITY_CHANGE_TEMPLATE, ACCOUNT_ACTIVITY_CHANGE_TEMPLATE,
true); true);
} else { } else {
LOG.warn(EMAIL_IGNORE_MSG, user.getEmail()); LOG.warn(EMAIL_IGNORE_MSG, userName);
} }
} }

View File

@ -48,6 +48,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
import org.openmetadata.common.utils.CommonUtil; import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration; import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration; import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration;
import org.openmetadata.schema.type.IndexMappingLanguage; import org.openmetadata.schema.type.IndexMappingLanguage;
import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jdbi3.CollectionDAO;
@ -208,6 +209,7 @@ public abstract class OpenMetadataApplicationTest {
nativeMigrationScriptsLocation, nativeMigrationScriptsLocation,
extensionMigrationScripsLocation, extensionMigrationScripsLocation,
null, null,
null,
false); false);
createIndices(); createIndices();
APP.before(); APP.before();
@ -221,6 +223,7 @@ public abstract class OpenMetadataApplicationTest {
String nativeMigrationSQLPath, String nativeMigrationSQLPath,
String extensionSQLScriptRootPath, String extensionSQLScriptRootPath,
PipelineServiceClientConfiguration pipelineServiceClientConfiguration, PipelineServiceClientConfiguration pipelineServiceClientConfiguration,
AuthenticationConfiguration authenticationConfiguration,
boolean forceMigrations) { boolean forceMigrations) {
DatasourceConfig.initialize(connType.label); DatasourceConfig.initialize(connType.label);
MigrationWorkflow workflow = MigrationWorkflow workflow =
@ -230,6 +233,7 @@ public abstract class OpenMetadataApplicationTest {
connType, connType,
extensionSQLScriptRootPath, extensionSQLScriptRootPath,
pipelineServiceClientConfiguration, pipelineServiceClientConfiguration,
authenticationConfiguration,
forceMigrations); forceMigrations);
// Initialize search repository // Initialize search repository
SearchRepository searchRepository = SearchRepository searchRepository =

View File

@ -28,7 +28,7 @@ public class MigrationWorkflowTest extends OpenMetadataApplicationTest {
migrationWorkflow = migrationWorkflow =
spy( spy(
new MigrationWorkflow( new MigrationWorkflow(
jdbi, "nativePath", ConnectionType.MYSQL, "extensionPath", null, false)); jdbi, "nativePath", ConnectionType.MYSQL, "extensionPath", null, null, false));
omMigrationList = omMigrationList =
List.of( List.of(
@ -37,18 +37,21 @@ public class MigrationWorkflowTest extends OpenMetadataApplicationTest {
null, null,
ConnectionType.MYSQL, ConnectionType.MYSQL,
migrationWorkflow.getPipelineServiceClientConfiguration(), migrationWorkflow.getPipelineServiceClientConfiguration(),
migrationWorkflow.getAuthenticationConfiguration(),
false), false),
new MigrationFile( new MigrationFile(
new File("/bootstrap/sql/migrations/native/1.2.0"), new File("/bootstrap/sql/migrations/native/1.2.0"),
null, null,
ConnectionType.MYSQL, ConnectionType.MYSQL,
migrationWorkflow.getPipelineServiceClientConfiguration(), migrationWorkflow.getPipelineServiceClientConfiguration(),
migrationWorkflow.getAuthenticationConfiguration(),
false), false),
new MigrationFile( new MigrationFile(
new File("/bootstrap/sql/migrations/native/1.2.1"), new File("/bootstrap/sql/migrations/native/1.2.1"),
null, null,
ConnectionType.MYSQL, ConnectionType.MYSQL,
migrationWorkflow.getPipelineServiceClientConfiguration(), migrationWorkflow.getPipelineServiceClientConfiguration(),
migrationWorkflow.getAuthenticationConfiguration(),
false)); false));
collateMigrationList = collateMigrationList =
@ -58,12 +61,14 @@ public class MigrationWorkflowTest extends OpenMetadataApplicationTest {
null, null,
ConnectionType.MYSQL, ConnectionType.MYSQL,
migrationWorkflow.getPipelineServiceClientConfiguration(), migrationWorkflow.getPipelineServiceClientConfiguration(),
migrationWorkflow.getAuthenticationConfiguration(),
true), true),
new MigrationFile( new MigrationFile(
new File("/bootstrap-collate/sql/migrations/native/1.2.2-collate"), new File("/bootstrap-collate/sql/migrations/native/1.2.2-collate"),
null, null,
ConnectionType.MYSQL, ConnectionType.MYSQL,
migrationWorkflow.getPipelineServiceClientConfiguration(), migrationWorkflow.getPipelineServiceClientConfiguration(),
migrationWorkflow.getAuthenticationConfiguration(),
true)); true));
} }
@ -72,18 +77,19 @@ public class MigrationWorkflowTest extends OpenMetadataApplicationTest {
Mockito.doReturn(omMigrationList) Mockito.doReturn(omMigrationList)
.when(migrationWorkflow) .when(migrationWorkflow)
.getMigrationFilesFromPath( .getMigrationFilesFromPath(
eq("nativePath"), any(ConnectionType.class), eq(null), eq(false)); eq("nativePath"), any(ConnectionType.class), eq(null), eq(null), eq(false));
Mockito.doReturn(collateMigrationList) Mockito.doReturn(collateMigrationList)
.when(migrationWorkflow) .when(migrationWorkflow)
.getMigrationFilesFromPath( .getMigrationFilesFromPath(
eq("extensionPath"), any(ConnectionType.class), eq(null), eq(true)); eq("extensionPath"), any(ConnectionType.class), eq(null), eq(null), eq(true));
List<MigrationFile> foundList = List<MigrationFile> foundList =
migrationWorkflow.getMigrationFiles( migrationWorkflow.getMigrationFiles(
"nativePath", "nativePath",
ConnectionType.MYSQL, ConnectionType.MYSQL,
"extensionPath", "extensionPath",
migrationWorkflow.getPipelineServiceClientConfiguration()); migrationWorkflow.getPipelineServiceClientConfiguration(),
migrationWorkflow.getAuthenticationConfiguration());
assertEquals( assertEquals(
List.of("1.1.0", "1.1.0-collate", "1.2.0", "1.2.1", "1.2.2-collate"), List.of("1.1.0", "1.1.0-collate", "1.2.0", "1.2.1", "1.2.2-collate"),