diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java index b121b61947..3e260bb150 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/local/LocalAuthProviderConstants.java @@ -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"; } diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle.properties index d6918a3638..a0996bff58 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle.properties +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle.properties @@ -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 @@ -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. diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle_ru.properties b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle_ru.properties index ce6b56819e..fdf8ada433 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle_ru.properties +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/OSGI-INF/l10n/bundle_ru.properties @@ -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 = Связать пароль пользователя @@ -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, который будет использоваться в качестве логина пользователя. Атрибут должен быть уникальным. Настройка привязки пользователя обязательна для использования этого параметра. \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml index 18e0023dab..ed76a9973b 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml @@ -27,11 +27,13 @@ features="password" required="false"/> + - + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java index 7bdee38129..cf8fd4e180 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java @@ -35,10 +35,9 @@ 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; import java.util.Hashtable; import java.util.Map; @@ -72,44 +71,58 @@ public Map authExternalUser( LdapSettings ldapSettings = new LdapSettings(providerConfig); Hashtable environment = creteAuthEnvironment(ldapSettings); - String fullUserDN = userName; + Map 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 validateAndLoginUserAccessByUsername( + @NotNull String login, + @NotNull String password, + @NotNull LdapSettings ldapSettings + ) throws DBException { + if ( + CommonUtils.isEmpty(ldapSettings.getBindUserDN()) + || CommonUtils.isEmpty(ldapSettings.getBindUserPassword()) + ) { + return null; + } + Hashtable 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 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()) ) { return; } @@ -117,13 +130,10 @@ private void validateUserAccess(@NotNull String fullUserDN, @NotNull LdapSetting 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"); @@ -132,14 +142,6 @@ private void validateUserAccess(@NotNull String fullUserDN, @NotNull LdapSetting 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); - } - } } } @@ -153,6 +155,53 @@ private static Hashtable creteAuthEnvironment(LdapSettings ldapS 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 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 { @@ -180,7 +229,11 @@ public DBWUserIdentity getUserIdentity( 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 @@ -239,4 +292,61 @@ public void refreshSession( public Object getInputUsername(@NotNull Map 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 authenticateLdap( + String userDN, + String password, + LdapSettings ldapSettings, + @Nullable String login, + Hashtable environment + ) throws DBException { + environment.put(Context.SECURITY_PRINCIPAL, userDN); + environment.put(Context.SECURITY_CREDENTIALS, password); + DirContext userContext = null; + try { + userContext = new InitialDirContext(environment); + Map 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); + } + } + } + } + } diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java index dac3e989b1..8b18c1fb6e 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java @@ -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"; diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java index 538e2df1b3..79310dadad 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java @@ -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( @@ -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, "");; } @@ -85,4 +87,8 @@ public String getBindUserPassword() { public String getFilter() { return filter; } + + public String getLoginAttribute() { + return loginAttribute; + } }