Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CB-5954. Added logic for login by login attribute #3148

Merged
merged 17 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
public class LocalAuthProviderConstants {
public static final String PROVIDER_ID = "local";
public static final String CRED_USER = "user";
public static final String CRED_DISPLAY_NAME = "displayName";
public static final String CRED_PASSWORD = "password";
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ prop.auth.model.ldap.ldap-port.description = LDAP server port, default is 389
prop.auth.model.ldap.ldap-identifier-attr = User identifier attribute
prop.auth.model.ldap.ldap-identifier-attr.description = LDAP attribute used as a user ID. Will be automatically added to the beginning of the 'User DN' value during authorization if not explicitly specified
prop.auth.model.ldap.ldap-dn = Base Distinguished Name
prop.auth.model.ldap.ldap-dn.description = Base Distinguished Name applicable for all users, example: dc=myOrg,dc=com. Will be automatically added to the end of the 'User DN' value during authorization if not explicitly specified
prop.auth.model.ldap.ldap-dn.description = Base Distinguished Name applicable for all users, example: dc=myOrg,dc=com. Will be automatically added to the end of the 'User DN' value during authorization if not explicitly specified. Leave blank if you want to use the root DN.
prop.auth.model.ldap.ldap-bind-user = Bind User DN
prop.auth.model.ldap.ldap-bind-user.description = DN of user, who has permissions to search for users to check access to the application with the specified filter.
prop.auth.model.ldap.ldap-bind-user-pwd = Bind User Password
Expand All @@ -16,4 +16,6 @@ prop.auth.model.ldap.user-dn = User DN
prop.auth.model.ldap.user-dn.description = LDAP user name
prop.auth.model.ldap.password = User password
prop.auth.model.ldap.password.description = LDAP user password
prop.auth.model.ldap.ldap-login = User login parameter
prop.auth.model.ldap.ldap-login.description = LDAP attribute to be used as the user login. The attribute must be unique. Configuring the bind user is mandatory to use this parameter.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ prop.auth.model.ldap.ldap-port.description = Порт LDAP-сервера, по
prop.auth.model.ldap.ldap-identifier-attr = Атрибут идентификатора пользователя
prop.auth.model.ldap.ldap-identifier-attr.description = Атрибут LDAP, используемый в качестве идентификатора пользователя. Будет автоматически добавлен в начало значения 'User DN' при авторизации, если не указан явно
prop.auth.model.ldap.ldap-dn = Базовое отличительное имя
prop.auth.model.ldap.ldap-dn.description = Базовое отличительное имя, применимое ко всем пользователям, например: dc=myOrg,dc=com. Будет автоматически добавлено в конец значения 'User DN' при авторизации, если не указано явно
prop.auth.model.ldap.ldap-dn.description = Базовое отличительное имя, применимое ко всем пользователям, например: dc=myOrg,dc=com. Будет автоматически добавлено в конец значения 'User DN' при авторизации, если не указано явно. Оставьте пустым если хотите использовать root DN.
prop.auth.model.ldap.ldap-bind-user = Привязка DN пользователя
prop.auth.model.ldap.ldap-bind-user.description = DN пользователя, который имеет права на поиск пользователей для проверки доступа к приложению с указанным фильтром.
prop.auth.model.ldap.ldap-bind-user-pwd = Связать пароль пользователя
Expand All @@ -16,3 +16,5 @@ prop.auth.model.ldap.user-dn = Имя пользователя DN
prop.auth.model.ldap.user-dn.description = LDAP имя пользователя
prop.auth.model.ldap.password = Пароль пользователя
prop.auth.model.ldap.password.description = LDAP пароль пользователя
prop.auth.model.ldap.ldap-login = Параметр логина
prop.auth.model.ldap.ldap-login.description = Атрибут LDAP, который будет использоваться в качестве логина пользователя. Атрибут должен быть уникальным. Настройка привязки пользователя обязательна для использования этого параметра.
4 changes: 3 additions & 1 deletion server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
features="password" required="false"/>
<property id="ldap-filter" label="%ldap-filter" type="string" required="false"
description="%ldap-filter.description"/>
<property id="ldap-login" label="%ldap-login" type="string" required="false"
description="%ldap-login.description"/>
</propertyGroup>
</configuration>
<credentials>
<propertyGroup label="Auth credentials">
<property id="user-dn" label="User DN" type="string" description="LDAP user name" user="true"/>
<property id="user-dn" label="User login" type="string" description="LDAP user name" user="true"/>
<property id="password" label="User password" type="string" description="LDAP user password"
user="true" encryption="plain"/>
</propertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,12 @@
import org.jkiss.utils.CommonUtils;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.*;
import java.util.HashMap;

Check warning on line 41 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View workflow job for this annotation

GitHub Actions / Server / Lint

[checkstyle] reported by reviewdog 🐶 Wrong lexicographical order for 'java.util.HashMap' import. Should be before 'javax.naming.directory.*'. Raw Output: /github/workspace/./server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java:41:1: warning: Wrong lexicographical order for 'java.util.HashMap' import. Should be before 'javax.naming.directory.*'. (com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck)
import java.util.Hashtable;

Check warning on line 42 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View workflow job for this annotation

GitHub Actions / Server / Lint

[checkstyle] reported by reviewdog 🐶 Wrong lexicographical order for 'java.util.Hashtable' import. Should be before 'javax.naming.directory.*'. Raw Output: /github/workspace/./server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java:42:1: warning: Wrong lexicographical order for 'java.util.Hashtable' import. Should be before 'javax.naming.directory.*'. (com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck)
import java.util.Map;

Check warning on line 43 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View workflow job for this annotation

GitHub Actions / Server / Lint

[checkstyle] reported by reviewdog 🐶 Wrong lexicographical order for 'java.util.Map' import. Should be before 'javax.naming.directory.*'. Raw Output: /github/workspace/./server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java:43:1: warning: Wrong lexicographical order for 'java.util.Map' import. Should be before 'javax.naming.directory.*'. (com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck)
import java.util.UUID;

public class LdapAuthProvider implements SMAuthProviderExternal<SMSession>, SMBruteForceProtected {
Expand Down Expand Up @@ -72,58 +71,69 @@
LdapSettings ldapSettings = new LdapSettings(providerConfig);
Hashtable<String, String> environment = creteAuthEnvironment(ldapSettings);

String fullUserDN = userName;
Map<String, Object> userData = null;
if (!isFullDN(userName) && CommonUtils.isNotEmpty(ldapSettings.getLoginAttribute())) {
userData = validateAndLoginUserAccessByUsername(userName, password, ldapSettings);

if (!fullUserDN.startsWith(ldapSettings.getUserIdentifierAttr())) {
fullUserDN = String.join("=", ldapSettings.getUserIdentifierAttr(), userName);
}
if (CommonUtils.isNotEmpty(ldapSettings.getBaseDN()) && !fullUserDN.endsWith(ldapSettings.getBaseDN())) {
fullUserDN = String.join(",", fullUserDN, ldapSettings.getBaseDN());
if (userData == null) {
String fullUserDN = buildFullUserDN(userName, ldapSettings);
validateUserAccess(fullUserDN, ldapSettings);
userData = authenticateLdap(fullUserDN, password, ldapSettings, null, environment);
}
return userData;
}
/**
* Find user and validate in ldap by uniq parameter from identityProviders
*
*/
private Map<String, Object> validateAndLoginUserAccessByUsername(

Check warning on line 90 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View workflow job for this annotation

GitHub Actions / Server / Lint

[checkstyle] reported by reviewdog 🐶 'METHOD_DEF' should be separated from previous line. Raw Output: /github/workspace/./server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java:90:5: warning: 'METHOD_DEF' should be separated from previous line. (com.puppycrawl.tools.checkstyle.checks.whitespace.EmptyLineSeparatorCheck)

Check warning on line 90 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View check run for this annotation

Jenkins-CI-integration / CheckStyle Java Report

server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java#L90

METHOD_DEF should be separated from previous line.
@NotNull String login,
@NotNull String password,
@NotNull LdapSettings ldapSettings
) throws DBException {
if (
CommonUtils.isEmpty(ldapSettings.getBindUserDN())
|| CommonUtils.isEmpty(ldapSettings.getBindUserPassword())
) {
return null;
}
Hashtable<String, String> serviceUserContext = creteAuthEnvironment(ldapSettings);
serviceUserContext.put(Context.SECURITY_PRINCIPAL, ldapSettings.getBindUserDN());
serviceUserContext.put(Context.SECURITY_CREDENTIALS, ldapSettings.getBindUserPassword());
DirContext serviceContext;

validateUserAccess(fullUserDN, ldapSettings);

environment.put(Context.SECURITY_PRINCIPAL, fullUserDN);
environment.put(Context.SECURITY_CREDENTIALS, password);
DirContext context = null;
try {
context = new InitialDirContext(environment);
Map<String, Object> userData = new HashMap<>();
userData.put(LdapConstants.CRED_USERNAME, findUserNameFromDN(fullUserDN, ldapSettings));
userData.put(LdapConstants.CRED_SESSION_ID, UUID.randomUUID());
return userData;
serviceContext = new InitialDirContext(serviceUserContext);
String userDN = findUserDN(serviceContext, ldapSettings, login);
if (userDN == null) {
return null;
}
return authenticateLdap(userDN, password, ldapSettings, login, creteAuthEnvironment(ldapSettings));
} catch (Exception e) {
throw new DBException("LDAP authentication failed: " + e.getMessage(), e);
} finally {
try {
if (context != null) {
context.close();
}
} catch (NamingException e) {
log.warn("Error closing LDAP user context", e);
}
}
}

/**
* Find user and validate in ldap by fullUserDN
*/
private void validateUserAccess(@NotNull String fullUserDN, @NotNull LdapSettings ldapSettings) throws DBException {
if (
CommonUtils.isEmpty(ldapSettings.getFilter())
|| CommonUtils.isEmpty(ldapSettings.getBindUserDN())
|| CommonUtils.isEmpty(ldapSettings.getBindUserPassword())
|| CommonUtils.isEmpty(ldapSettings.getBindUserDN())
|| CommonUtils.isEmpty(ldapSettings.getBindUserPassword())

Check warning on line 125 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View workflow job for this annotation

GitHub Actions / Server / Lint

[checkstyle] reported by reviewdog 🐶 'if' child has incorrect indentation level 12, expected level should be 16. Raw Output: /github/workspace/./server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java:125:13: warning: 'if' child has incorrect indentation level 12, expected level should be 16. (com.puppycrawl.tools.checkstyle.checks.indentation.IndentationCheck)

Check warning on line 125 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View check run for this annotation

Jenkins-CI-integration / CheckStyle Java Report

server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java#L125

if child has incorrect indentation level 12, expected level should be 16.
) {
return;
}

var environment = creteAuthEnvironment(ldapSettings);
environment.put(Context.SECURITY_PRINCIPAL, ldapSettings.getBindUserDN());
environment.put(Context.SECURITY_CREDENTIALS, ldapSettings.getBindUserPassword());
DirContext bindUserContext = null;
DirContext bindUserContext;
try {
bindUserContext = new InitialDirContext(environment);

SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setTimeLimit(30_000);
SearchControls searchControls = createSearchControls();
var searchResult = bindUserContext.search(fullUserDN, ldapSettings.getFilter(), searchControls);
if (!searchResult.hasMore()) {
throw new DBException("Access denied");
Expand All @@ -132,14 +142,6 @@
throw e;
} catch (Exception e) {
throw new DBException("LDAP user access validation by filter failed: " + e.getMessage(), e);
} finally {
if (bindUserContext != null) {
try {
bindUserContext.close();
} catch (NamingException e) {
log.warn("Error closing LDAP bind user context", e);
}
}
}
}

Expand All @@ -153,9 +155,56 @@
return environment;
}

private String findUserDN(DirContext serviceContext, LdapSettings ldapSettings, String userIdentifier) throws DBException {
try {
String searchFilter = buildSearchFilter(ldapSettings, userIdentifier);
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setReturningAttributes(new String[]{"distinguishedName"});
String baseDN = getBaseDN(serviceContext, ldapSettings);

NamingEnumeration<SearchResult> results = serviceContext.search(baseDN, searchFilter, searchControls);

if (results.hasMore()) {
return results.next().getNameInNamespace();
}
return null;
} catch (Exception e) {
throw new DBException("Error finding user DN: " + e.getMessage(), e);
}
}

private String getBaseDN(DirContext serviceContext, LdapSettings ldapSettings) throws DBException {
if (CommonUtils.isEmpty(ldapSettings.getBaseDN())) {
return getRootDN(serviceContext);
}
return ldapSettings.getBaseDN();
}

private String buildSearchFilter(LdapSettings ldapSettings, String userIdentifier) {
String userFilter = String.format("(%s=%s)", ldapSettings.getLoginAttribute(), userIdentifier);
if (CommonUtils.isNotEmpty(ldapSettings.getFilter())) {
return String.format("(&%s%s)", userFilter, ldapSettings.getFilter());
}
return userFilter;
}

private String getRootDN(DirContext adminContext) throws DBException {
try {
Attributes attributes = adminContext.getAttributes("", new String[]{"namingContexts"});
Attribute namingContexts = attributes.get("namingContexts");
if (namingContexts != null && namingContexts.size() > 0) {
return (String) namingContexts.get(0);
}
throw new DBException("Root DN not found in namingContexts");
} catch (Exception e) {
throw new DBException("Error retrieving root DN: " + e.getMessage(), e);
}
}

@NotNull
private String findUserNameFromDN(@NotNull String fullUserDN, @NotNull LdapSettings ldapSettings)
throws DBException {

Check warning on line 207 in server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java

View workflow job for this annotation

GitHub Actions / Server / Lint

[checkstyle] reported by reviewdog 🐶 'throws' has incorrect indentation level 8, expected level should be 12. Raw Output: /github/workspace/./server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java:207:9: warning: 'throws' has incorrect indentation level 8, expected level should be 12. (com.puppycrawl.tools.checkstyle.checks.indentation.IndentationCheck)
String userId = null;
for (String dn : fullUserDN.split(",")) {
if (dn.startsWith(ldapSettings.getUserIdentifierAttr() + "=")) {
Expand All @@ -180,7 +229,11 @@
if (CommonUtils.isEmpty(userName)) {
throw new DBException("LDAP user name is empty");
}
return new DBWUserIdentity(userName, userName);
String displayName = JSONUtils.getString(authParameters, LocalAuthProviderConstants.CRED_DISPLAY_NAME);
if (CommonUtils.isEmpty(displayName)) {
displayName = userName;
}
return new DBWUserIdentity(userName, displayName);
}

@Nullable
Expand Down Expand Up @@ -239,4 +292,61 @@
public Object getInputUsername(@NotNull Map<String, Object> cred) {
return cred.get(LdapConstants.CRED_USERNAME);
}

private boolean isFullDN(String userName) {
return userName.contains(",") && userName.contains("=");
}

private String buildFullUserDN(String userName, LdapSettings ldapSettings) {
String fullUserDN = userName;

if (!fullUserDN.startsWith(ldapSettings.getUserIdentifierAttr())) {
fullUserDN = String.join("=", ldapSettings.getUserIdentifierAttr(), userName);
}
if (CommonUtils.isNotEmpty(ldapSettings.getBaseDN()) && !fullUserDN.endsWith(ldapSettings.getBaseDN())) {
fullUserDN = String.join(",", fullUserDN, ldapSettings.getBaseDN());
}

return fullUserDN;
}

private SearchControls createSearchControls() {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setTimeLimit(30_000);
return searchControls;
}

private Map<String, Object> authenticateLdap(
String userDN,
String password,
LdapSettings ldapSettings,
@Nullable String login,
Hashtable<String, String> environment
) throws DBException {
environment.put(Context.SECURITY_PRINCIPAL, userDN);
environment.put(Context.SECURITY_CREDENTIALS, password);
DirContext userContext = null;
try {
userContext = new InitialDirContext(environment);
Map<String, Object> userData = new HashMap<>();
userData.put(LdapConstants.CRED_USERNAME, findUserNameFromDN(userDN, ldapSettings));
userData.put(LdapConstants.CRED_SESSION_ID, UUID.randomUUID());
if (login != null) {
userData.put(LdapConstants.CRED_DISPLAY_NAME, login);
}
return userData;
} catch (Exception e) {
throw new DBException("LDAP authentication failed: " + e.getMessage(), e);
} finally {
if (userContext != null) {
try {
userContext.close();
} catch (NamingException e) {
log.warn("Error closing LDAP user context", e);
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ public interface LdapConstants {
String PARAM_BIND_USER_PASSWORD = "ldap-bind-user-pwd";
String PARAM_FILTER = "ldap-filter";
String PARAM_USER_IDENTIFIER_ATTR = "ldap-identifier-attr";
String PARAM_LOGIN = "ldap-login";


String CRED_USERNAME = "user";
String CRED_DISPLAY_NAME = "displayName";
String CRED_USER_DN = "user-dn";
String CRED_PASSWORD = "password";
String CRED_SESSION_ID = "session-id";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class LdapSettings {
private final String bindUser;
private final String bindUserPassword;
private final String filter;
private final String loginAttribute;


protected LdapSettings(
Expand All @@ -48,6 +49,7 @@ protected LdapSettings(
this.bindUser = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_BIND_USER, "");
this.bindUserPassword = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_BIND_USER_PASSWORD, "");
this.filter = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_FILTER, "");
this.loginAttribute = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_LOGIN, "");;
}


Expand Down Expand Up @@ -85,4 +87,8 @@ public String getBindUserPassword() {
public String getFilter() {
return filter;
}

public String getLoginAttribute() {
return loginAttribute;
}
}
Loading