From a22087da3ca968ed5a592ed03d47f282b6b59d63 Mon Sep 17 00:00:00 2001 From: Aurelien Poscia Date: Tue, 25 Oct 2022 12:03:35 +0200 Subject: SONAR-17508 Decouple LDAP authentication from sonar-plugin-API --- .../sonar/auth/ldap/DefaultLdapAuthenticator.java | 145 +++++++++++ .../sonar/auth/ldap/DefaultLdapGroupsProvider.java | 139 +++++++++++ .../sonar/auth/ldap/DefaultLdapUsersProvider.java | 120 +++++++++ .../org/sonar/auth/ldap/LdapAuthenticator.java | 127 +++------- .../org/sonar/auth/ldap/LdapGroupsProvider.java | 124 +--------- .../main/java/org/sonar/auth/ldap/LdapRealm.java | 29 +-- .../java/org/sonar/auth/ldap/LdapUserDetails.java | 75 ++++++ .../org/sonar/auth/ldap/LdapUsersProvider.java | 107 ++------- .../auth/ldap/DefaultLdapAuthenticatorTest.java | 130 ++++++++++ .../auth/ldap/DefaultLdapGroupsProviderTest.java | 149 ++++++++++++ .../auth/ldap/DefaultLdapUsersProviderTest.java | 76 ++++++ .../java/org/sonar/auth/ldap/KerberosTest.java | 10 +- .../org/sonar/auth/ldap/LdapAuthenticatorTest.java | 129 ---------- .../auth/ldap/LdapAutoDiscoveryWarningLogTest.java | 3 - .../sonar/auth/ldap/LdapGroupsProviderTest.java | 149 ------------ .../java/org/sonar/auth/ldap/LdapRealmTest.java | 20 +- .../org/sonar/auth/ldap/LdapUsersProviderTest.java | 77 ------ server/sonar-webserver-auth/build.gradle | 1 + .../authentication/AuthenticationModule.java | 1 + .../authentication/CredentialsAuthentication.java | 8 +- .../LdapCredentialsAuthentication.java | 184 ++++++++++++++ .../server/authentication/UserRegistration.java | 4 +- .../sonar/server/user/SecurityRealmFactory.java | 12 +- .../CredentialsAuthenticationTest.java | 27 ++- .../LdapCredentialsAuthenticationTest.java | 267 +++++++++++++++++++++ .../server/user/SecurityRealmFactoryTest.java | 9 + 26 files changed, 1419 insertions(+), 703 deletions(-) create mode 100644 server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapAuthenticator.java create mode 100644 server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapGroupsProvider.java create mode 100644 server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapUsersProvider.java create mode 100644 server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserDetails.java create mode 100644 server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapAuthenticatorTest.java create mode 100644 server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapGroupsProviderTest.java create mode 100644 server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapUsersProviderTest.java delete mode 100644 server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java delete mode 100644 server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java delete mode 100644 server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java create mode 100644 server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/LdapCredentialsAuthentication.java create mode 100644 server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java (limited to 'server') diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapAuthenticator.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapAuthenticator.java new file mode 100644 index 00000000000..b4d2479c3fa --- /dev/null +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapAuthenticator.java @@ -0,0 +1,145 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.ldap; + +import java.util.Map; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchResult; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +/** + * @author Evgeny Mandrikov + */ +@ServerSide +public class DefaultLdapAuthenticator implements LdapAuthenticator { + + private static final Logger LOG = Loggers.get(DefaultLdapAuthenticator.class); + private final Map contextFactories; + private final Map userMappings; + + public DefaultLdapAuthenticator(Map contextFactories, Map userMappings) { + this.contextFactories = contextFactories; + this.userMappings = userMappings; + } + + @Override + public boolean doAuthenticate(Context context) { + return authenticate(context.getUsername(), context.getPassword()); + } + + /** + * Authenticate the user against LDAP servers until first success. + * + * @param login The login to use. + * @param password The password to use. + * @return false if specified user cannot be authenticated with specified password on any LDAP server + */ + public boolean authenticate(String login, String password) { + for (Map.Entry ldapEntry : userMappings.entrySet()) { + String ldapKey = ldapEntry.getKey(); + LdapUserMapping ldapUserMapping = ldapEntry.getValue(); + LdapContextFactory ldapContextFactory = contextFactories.get(ldapKey); + final String principal; + if (ldapContextFactory.isSasl()) { + principal = login; + } else { + SearchResult result = findUser(login, ldapKey, ldapUserMapping, ldapContextFactory); + if (result == null) { + continue; + } + principal = result.getNameInNamespace(); + } + boolean passwordValid = isPasswordValid(password, ldapKey, ldapContextFactory, principal); + if (passwordValid) { + return true; + } + } + LOG.debug("User {} not found", login); + return false; + } + + private static SearchResult findUser(String login, String ldapKey, LdapUserMapping ldapUserMapping, LdapContextFactory ldapContextFactory) { + SearchResult result; + try { + result = ldapUserMapping.createSearch(ldapContextFactory, login).findUnique(); + } catch (NamingException e) { + LOG.debug("User {} not found in server {}: {}", login, ldapKey, e.getMessage()); + return null; + } + if (result == null) { + LOG.debug("User {} not found in {}", login, ldapKey); + return null; + } + return result; + } + + private boolean isPasswordValid(String password, String ldapKey, LdapContextFactory ldapContextFactory, String principal) { + if (ldapContextFactory.isGssapi()) { + return checkPasswordUsingGssapi(principal, password, ldapKey); + } + return checkPasswordUsingBind(principal, password, ldapKey); + } + + private boolean checkPasswordUsingBind(String principal, String password, String ldapKey) { + if (StringUtils.isEmpty(password)) { + LOG.debug("Password is blank."); + return false; + } + InitialDirContext context = null; + try { + context = contextFactories.get(ldapKey).createUserContext(principal, password); + return true; + } catch (NamingException e) { + LOG.debug("Password not valid for user {} in server {}: {}", principal, ldapKey, e.getMessage()); + return false; + } finally { + ContextHelper.closeQuietly(context); + } + } + + private boolean checkPasswordUsingGssapi(String principal, String password, String ldapKey) { + // Use our custom configuration to avoid reliance on external config + Configuration.setConfiguration(new Krb5LoginConfiguration()); + LoginContext lc; + try { + lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, password)); + lc.login(); + } catch (LoginException e) { + // Bad username: Client not found in Kerberos database + // Bad password: Integrity check on decrypted field failed + LOG.debug("Password not valid for {} in server {}: {}", principal, ldapKey, e.getMessage()); + return false; + } + try { + lc.logout(); + } catch (LoginException e) { + LOG.warn("Logout fails", e); + } + return true; + } + +} diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapGroupsProvider.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapGroupsProvider.java new file mode 100644 index 00000000000..0a4195133c4 --- /dev/null +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapGroupsProvider.java @@ -0,0 +1,139 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.ldap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchResult; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static java.lang.String.format; + +/** + * @author Evgeny Mandrikov + */ +@ServerSide +public class DefaultLdapGroupsProvider implements LdapGroupsProvider { + + private static final Logger LOG = Loggers.get(DefaultLdapGroupsProvider.class); + + private final Map contextFactories; + private final Map userMappings; + private final Map groupMappings; + + public DefaultLdapGroupsProvider(Map contextFactories, Map userMappings, Map groupMapping) { + this.contextFactories = contextFactories; + this.userMappings = userMappings; + this.groupMappings = groupMapping; + } + + @Override + public Collection doGetGroups(Context context) { + return getGroups(context.getUsername()); + } + + /** + * @throws LdapException if unable to retrieve groups + */ + public Collection getGroups(String username) { + checkPrerequisites(username); + Set groups = new HashSet<>(); + List exceptions = new ArrayList<>(); + for (String serverKey : userMappings.keySet()) { + if (groupMappings.containsKey(serverKey)) { + SearchResult searchResult = searchUserGroups(username, exceptions, serverKey); + if (searchResult != null) { + try { + NamingEnumeration result = groupMappings + .get(serverKey) + .createSearch(contextFactories.get(serverKey), searchResult).find(); + groups.addAll(mapGroups(serverKey, result)); + // if no exceptions occur, we found the user and his groups and mapped his details. + break; + } catch (NamingException e) { + // just in case if Sonar silently swallowed exception + LOG.debug(e.getMessage(), e); + exceptions.add(new LdapException(format("Unable to retrieve groups for user %s in %s", username, serverKey), e)); + } + } + } + } + checkResults(groups, exceptions); + return groups; + } + + private static void checkResults(Set groups, List exceptions) { + if (groups.isEmpty() && !exceptions.isEmpty()) { + // No groups found and there is an exception so there is a reason the user could not be found. + throw exceptions.iterator().next(); + } + } + + private void checkPrerequisites(String username) { + if (userMappings.isEmpty() || groupMappings.isEmpty()) { + throw new LdapException(format("Unable to retrieve details for user %s: No user or group mapping found.", username)); + } + } + + private SearchResult searchUserGroups(String username, List exceptions, String serverKey) { + SearchResult searchResult = null; + try { + LOG.debug("Requesting groups for user {}", username); + + searchResult = userMappings.get(serverKey).createSearch(contextFactories.get(serverKey), username) + .returns(groupMappings.get(serverKey).getRequiredUserAttributes()) + .findUnique(); + } catch (NamingException e) { + // just in case if Sonar silently swallowed exception + LOG.debug(e.getMessage(), e); + exceptions.add(new LdapException(format("Unable to retrieve groups for user %s in %s", username, serverKey), e)); + } + return searchResult; + } + + /** + * Map all the groups. + * + * @param serverKey The index we use to choose the correct {@link LdapGroupMapping}. + * @param searchResult The {@link SearchResult} from the search for the user. + * @return A {@link Collection} of groups the user is member of. + * @throws NamingException + */ + private Collection mapGroups(String serverKey, NamingEnumeration searchResult) throws NamingException { + Set groups = new HashSet<>(); + while (searchResult.hasMoreElements()) { + SearchResult obj = searchResult.nextElement(); + Attributes attributes = obj.getAttributes(); + String groupId = (String) attributes.get(groupMappings.get(serverKey).getIdAttribute()).get(); + groups.add(groupId); + } + return groups; + } + +} diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapUsersProvider.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapUsersProvider.java new file mode 100644 index 00000000000..72d3a51e902 --- /dev/null +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapUsersProvider.java @@ -0,0 +1,120 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.ldap; + +import java.util.Map; +import javax.annotation.Nullable; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchResult; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static java.lang.String.format; + +/** + * @author Evgeny Mandrikov + */ +@ServerSide +public class DefaultLdapUsersProvider implements LdapUsersProvider { + + private static final Logger LOG = Loggers.get(DefaultLdapUsersProvider.class); + private final Map contextFactories; + private final Map userMappings; + + public DefaultLdapUsersProvider(Map contextFactories, Map userMappings) { + this.contextFactories = contextFactories; + this.userMappings = userMappings; + } + + private static String getAttributeValue(@Nullable Attribute attribute) throws NamingException { + if (attribute == null) { + return ""; + } + return (String) attribute.get(); + } + + @Override + public LdapUserDetails doGetUserDetails(Context context) { + return getUserDetails(context.getUsername()); + } + + /** + * @return details for specified user, or null if such user doesn't exist + * @throws LdapException if unable to retrieve details + */ + public LdapUserDetails getUserDetails(String username) { + LOG.debug("Requesting details for user {}", username); + // If there are no userMappings available, we can not retrieve user details. + if (userMappings.isEmpty()) { + String errorMessage = format("Unable to retrieve details for user %s: No user mapping found.", username); + LOG.debug(errorMessage); + throw new LdapException(errorMessage); + } + LdapUserDetails details = null; + LdapException exception = null; + for (Map.Entry serverEntry : userMappings.entrySet()) { + String serverKey = serverEntry.getKey(); + LdapUserMapping ldapUserMapping = serverEntry.getValue(); + + SearchResult searchResult = null; + try { + searchResult = ldapUserMapping.createSearch(contextFactories.get(serverKey), username) + .returns(ldapUserMapping.getEmailAttribute(), ldapUserMapping.getRealNameAttribute()) + .findUnique(); + } catch (NamingException e) { + // just in case if Sonar silently swallowed exception + LOG.debug(e.getMessage(), e); + exception = new LdapException("Unable to retrieve details for user " + username + " in " + serverKey, e); + } + if (searchResult != null) { + try { + details = mapUserDetails(ldapUserMapping, searchResult); + // if no exceptions occur, we found the user and mapped his details. + break; + } catch (NamingException e) { + // just in case if Sonar silently swallowed exception + LOG.debug(e.getMessage(), e); + exception = new LdapException("Unable to retrieve details for user " + username + " in " + serverKey, e); + } + } else { + // user not found + LOG.debug("User {} not found in {}", username, serverKey); + } + } + if (details == null && exception != null) { + // No user found and there is an exception so there is a reason the user could not be found. + throw exception; + } + return details; + } + + private static LdapUserDetails mapUserDetails(LdapUserMapping ldapUserMapping, SearchResult searchResult) throws NamingException { + Attributes attributes = searchResult.getAttributes(); + LdapUserDetails details; + details = new LdapUserDetails(); + details.setName(getAttributeValue(attributes.get(ldapUserMapping.getRealNameAttribute()))); + details.setEmail(getAttributeValue(attributes.get(ldapUserMapping.getEmailAttribute()))); + return details; + } + +} diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java index 0222a2aaeb6..ef2132b6c73 100644 --- a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java @@ -19,112 +19,47 @@ */ package org.sonar.auth.ldap; -import java.util.Map; -import javax.naming.NamingException; -import javax.naming.directory.InitialDirContext; -import javax.naming.directory.SearchResult; -import javax.security.auth.login.Configuration; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; -import org.apache.commons.lang.StringUtils; -import org.sonar.api.security.Authenticator; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; -/** - * @author Evgeny Mandrikov - */ -public class LdapAuthenticator extends Authenticator { - - private static final Logger LOG = Loggers.get(LdapAuthenticator.class); - private final Map contextFactories; - private final Map userMappings; - - public LdapAuthenticator(Map contextFactories, Map userMappings) { - this.contextFactories = contextFactories; - this.userMappings = userMappings; - } +import static java.util.Objects.requireNonNull; - @Override - public boolean doAuthenticate(Context context) { - return authenticate(context.getUsername(), context.getPassword()); - } +public interface LdapAuthenticator { /** - * Authenticate the user against LDAP servers until first success. - * @param login The login to use. - * @param password The password to use. - * @return false if specified user cannot be authenticated with specified password on any LDAP server + * @return true if user was successfully authenticated with specified credentials, false otherwise + * @throws RuntimeException in case of unexpected error such as connection failure */ - public boolean authenticate(String login, String password) { - for (String ldapKey : userMappings.keySet()) { - final String principal; - if (contextFactories.get(ldapKey).isSasl()) { - principal = login; - } else { - final SearchResult result; - try { - result = userMappings.get(ldapKey).createSearch(contextFactories.get(ldapKey), login).findUnique(); - } catch (NamingException e) { - LOG.debug("User {} not found in server {}: {}", login, ldapKey, e.getMessage()); - continue; - } - if (result == null) { - LOG.debug("User {} not found in {}", login, ldapKey); - continue; - } - principal = result.getNameInNamespace(); - } - boolean passwordValid; - if (contextFactories.get(ldapKey).isGssapi()) { - passwordValid = checkPasswordUsingGssapi(principal, password, ldapKey); - } else { - passwordValid = checkPasswordUsingBind(principal, password, ldapKey); - } - if (passwordValid) { - return true; - } - } - LOG.debug("User {} not found", login); - return false; - } + boolean doAuthenticate(LdapAuthenticator.Context context); + + final class Context { + private String username; + private String password; + private HttpServletRequest request; - private boolean checkPasswordUsingBind(String principal, String password, String ldapKey) { - if (StringUtils.isEmpty(password)) { - LOG.debug("Password is blank."); - return false; + public Context(@Nullable String username, @Nullable String password, HttpServletRequest request) { + requireNonNull(request); + this.request = request; + this.username = username; + this.password = password; } - InitialDirContext context = null; - try { - context = contextFactories.get(ldapKey).createUserContext(principal, password); - return true; - } catch (NamingException e) { - LOG.debug("Password not valid for user {} in server {}: {}", principal, ldapKey, e.getMessage()); - return false; - } finally { - ContextHelper.closeQuietly(context); + + /** + * Username can be null, for example when using CAS. + */ + public String getUsername() { + return username; } - } - private boolean checkPasswordUsingGssapi(String principal, String password, String ldapKey) { - // Use our custom configuration to avoid reliance on external config - Configuration.setConfiguration(new Krb5LoginConfiguration()); - LoginContext lc; - try { - lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, password)); - lc.login(); - } catch (LoginException e) { - // Bad username: Client not found in Kerberos database - // Bad password: Integrity check on decrypted field failed - LOG.debug("Password not valid for {} in server {}: {}", principal, ldapKey, e.getMessage()); - return false; + /** + * Password can be null, for example when using CAS. + */ + public String getPassword() { + return password; } - try { - lc.logout(); - } catch (LoginException e) { - LOG.warn("Logout fails", e); + + public HttpServletRequest getRequest() { + return request; } - return true; } - } diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java index 9bf4f6b79a5..3a3c2701294 100644 --- a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java @@ -19,126 +19,28 @@ */ package org.sonar.auth.ldap; -import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.directory.SearchResult; -import org.sonar.api.security.ExternalGroupsProvider; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; +import javax.servlet.http.HttpServletRequest; -import static java.lang.String.format; +public interface LdapGroupsProvider { -/** - * @author Evgeny Mandrikov - */ -public class LdapGroupsProvider extends ExternalGroupsProvider { - - private static final Logger LOG = Loggers.get(LdapGroupsProvider.class); + Collection doGetGroups(Context context); - private final Map contextFactories; - private final Map userMappings; - private final Map groupMappings; + final class Context { + private String username; + private HttpServletRequest request; - public LdapGroupsProvider(Map contextFactories, Map userMappings, Map groupMapping) { - this.contextFactories = contextFactories; - this.userMappings = userMappings; - this.groupMappings = groupMapping; - } - - @Override - public Collection doGetGroups(Context context) { - return getGroups(context.getUsername()); - } - - /** - * @throws LdapException if unable to retrieve groups - */ - public Collection getGroups(String username) { - checkPrerequisites(username); - Set groups = new HashSet<>(); - List exceptions = new ArrayList<>(); - for (String serverKey : userMappings.keySet()) { - if (!groupMappings.containsKey(serverKey)) { - // No group mapping for this ldap instance. - continue; - } - SearchResult searchResult = searchUserGroups(username, exceptions, serverKey); - - if (searchResult != null) { - try { - NamingEnumeration result = groupMappings - .get(serverKey) - .createSearch(contextFactories.get(serverKey), searchResult).find(); - groups.addAll(mapGroups(serverKey, result)); - // if no exceptions occur, we found the user and his groups and mapped his details. - break; - } catch (NamingException e) { - // just in case if Sonar silently swallowed exception - LOG.debug(e.getMessage(), e); - exceptions.add(new LdapException(format("Unable to retrieve groups for user %s in %s", username, serverKey), e)); - } - } else { - // user not found - continue; - } + public Context(String username, HttpServletRequest request) { + this.username = username; + this.request = request; } - checkResults(groups, exceptions); - return groups; - } - private static void checkResults(Set groups, List exceptions) { - if (groups.isEmpty() && !exceptions.isEmpty()) { - // No groups found and there is an exception so there is a reason the user could not be found. - throw exceptions.iterator().next(); + public String getUsername() { + return username; } - } - private void checkPrerequisites(String username) { - if (userMappings.isEmpty() || groupMappings.isEmpty()) { - throw new LdapException(format("Unable to retrieve details for user %s: No user or group mapping found.", username)); + public HttpServletRequest getRequest() { + return request; } } - - private SearchResult searchUserGroups(String username, List exceptions, String serverKey) { - SearchResult searchResult = null; - try { - LOG.debug("Requesting groups for user {}", username); - - searchResult = userMappings.get(serverKey).createSearch(contextFactories.get(serverKey), username) - .returns(groupMappings.get(serverKey).getRequiredUserAttributes()) - .findUnique(); - } catch (NamingException e) { - // just in case if Sonar silently swallowed exception - LOG.debug(e.getMessage(), e); - exceptions.add(new LdapException(format("Unable to retrieve groups for user %s in %s", username, serverKey), e)); - } - return searchResult; - } - - /** - * Map all the groups. - * - * @param serverKey The index we use to choose the correct {@link LdapGroupMapping}. - * @param searchResult The {@link SearchResult} from the search for the user. - * @return A {@link Collection} of groups the user is member of. - * @throws NamingException - */ - private Collection mapGroups(String serverKey, NamingEnumeration searchResult) throws NamingException { - Set groups = new HashSet<>(); - while (searchResult.hasMoreElements()) { - SearchResult obj = searchResult.nextElement(); - Attributes attributes = obj.getAttributes(); - String groupId = (String) attributes.get(groupMappings.get(serverKey).getIdAttribute()).get(); - groups.add(groupId); - } - return groups; - } - } diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java index 5c4b7fbd034..1b03964ea58 100644 --- a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java @@ -20,15 +20,13 @@ package org.sonar.auth.ldap; import java.util.Map; -import org.sonar.api.security.Authenticator; -import org.sonar.api.security.ExternalGroupsProvider; -import org.sonar.api.security.ExternalUsersProvider; -import org.sonar.api.security.SecurityRealm; +import org.sonar.api.server.ServerSide; /** * @author Evgeny Mandrikov */ -public class LdapRealm extends SecurityRealm { +@ServerSide +public class LdapRealm { private LdapUsersProvider usersProvider; private LdapGroupsProvider groupsProvider; @@ -39,43 +37,34 @@ public class LdapRealm extends SecurityRealm { this.settingsManager = settingsManager; } - @Override - public String getName() { - return "LDAP"; - } - /** * Initializes LDAP realm and tests connection. * * @throws LdapException if a NamingException was thrown during test */ - @Override public void init() { Map contextFactories = settingsManager.getContextFactories(); Map userMappings = settingsManager.getUserMappings(); - usersProvider = new LdapUsersProvider(contextFactories, userMappings); - authenticator = new LdapAuthenticator(contextFactories, userMappings); + usersProvider = new DefaultLdapUsersProvider(contextFactories, userMappings); + authenticator = new DefaultLdapAuthenticator(contextFactories, userMappings); Map groupMappings = settingsManager.getGroupMappings(); if (!groupMappings.isEmpty()) { - groupsProvider = new LdapGroupsProvider(contextFactories, userMappings, groupMappings); + groupsProvider = new DefaultLdapGroupsProvider(contextFactories, userMappings, groupMappings); } for (LdapContextFactory contextFactory : contextFactories.values()) { contextFactory.testConnection(); } } - @Override - public Authenticator doGetAuthenticator() { + public LdapAuthenticator doGetAuthenticator() { return authenticator; } - @Override - public ExternalUsersProvider getUsersProvider() { + public LdapUsersProvider getUsersProvider() { return usersProvider; } - @Override - public ExternalGroupsProvider getGroupsProvider() { + public LdapGroupsProvider getGroupsProvider() { return groupsProvider; } diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserDetails.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserDetails.java new file mode 100644 index 00000000000..251f8e65ed9 --- /dev/null +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserDetails.java @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.ldap; + +import org.sonar.api.security.ExternalUsersProvider; + +/** + * This class is not intended to be subclassed by clients. + * + * @see ExternalUsersProvider + * @since 2.14 + */ +public final class LdapUserDetails { + + private String name = ""; + private String email = ""; + private String userId = ""; + + public void setEmail(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + /** + * @since 5.2 + */ + public void setUserId(String userId) { + this.userId = userId; + } + + /** + * @since 5.2 + */ + public String getUserId() { + return userId; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("UserDetails{"); + sb.append("name='").append(name).append('\''); + sb.append(", email='").append(email).append('\''); + sb.append(", userId='").append(userId).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java index 360cd4b3560..4b1870e4bbc 100644 --- a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java +++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java @@ -19,107 +19,28 @@ */ package org.sonar.auth.ldap; -import java.util.Map; import javax.annotation.Nullable; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.SearchResult; -import org.sonar.api.security.ExternalUsersProvider; -import org.sonar.api.security.UserDetails; -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; +import javax.servlet.http.HttpServletRequest; -import static java.lang.String.format; +public interface LdapUsersProvider { -/** - * @author Evgeny Mandrikov - */ -public class LdapUsersProvider extends ExternalUsersProvider { - - private static final Logger LOG = Loggers.get(LdapUsersProvider.class); - private final Map contextFactories; - private final Map userMappings; + LdapUserDetails doGetUserDetails(Context context); - public LdapUsersProvider(Map contextFactories, Map userMappings) { - this.contextFactories = contextFactories; - this.userMappings = userMappings; - } + final class Context { + private String username; + private HttpServletRequest request; - private static String getAttributeValue(@Nullable Attribute attribute) throws NamingException { - if (attribute == null) { - return ""; + public Context(@Nullable String username, HttpServletRequest request) { + this.username = username; + this.request = request; } - return (String) attribute.get(); - } - - @Override - public UserDetails doGetUserDetails(Context context) { - return getUserDetails(context.getUsername()); - } - /** - * @return details for specified user, or null if such user doesn't exist - * @throws LdapException if unable to retrieve details - */ - public UserDetails getUserDetails(String username) { - LOG.debug("Requesting details for user {}", username); - // If there are no userMappings available, we can not retrieve user details. - if (userMappings.isEmpty()) { - String errorMessage = format("Unable to retrieve details for user %s: No user mapping found.", username); - LOG.debug(errorMessage); - throw new LdapException(errorMessage); + public String getUsername() { + return username; } - UserDetails details = null; - LdapException exception = null; - for (String serverKey : userMappings.keySet()) { - SearchResult searchResult = null; - try { - searchResult = userMappings.get(serverKey).createSearch(contextFactories.get(serverKey), username) - .returns(userMappings.get(serverKey).getEmailAttribute(), userMappings.get(serverKey).getRealNameAttribute()) - .findUnique(); - } catch (NamingException e) { - // just in case if Sonar silently swallowed exception - LOG.debug(e.getMessage(), e); - exception = new LdapException("Unable to retrieve details for user " + username + " in " + serverKey, e); - } - if (searchResult != null) { - try { - details = mapUserDetails(serverKey, searchResult); - // if no exceptions occur, we found the user and mapped his details. - break; - } catch (NamingException e) { - // just in case if Sonar silently swallowed exception - LOG.debug(e.getMessage(), e); - exception = new LdapException("Unable to retrieve details for user " + username + " in " + serverKey, e); - } - } else { - // user not found - LOG.debug("User {} not found in {}", username, serverKey); - continue; - } - } - if (details == null && exception != null) { - // No user found and there is an exception so there is a reason the user could not be found. - throw exception; - } - return details; - } - /** - * Map the properties from LDAP to the {@link UserDetails} - * - * @param serverKey the LDAP index so we use the correct {@link LdapUserMapping} - * @return If no exceptions are thrown, a {@link UserDetails} object containing the values from LDAP. - * @throws NamingException In case the communication or mapping to the LDAP server fails. - */ - private UserDetails mapUserDetails(String serverKey, SearchResult searchResult) throws NamingException { - Attributes attributes = searchResult.getAttributes(); - UserDetails details; - details = new UserDetails(); - details.setName(getAttributeValue(attributes.get(userMappings.get(serverKey).getRealNameAttribute()))); - details.setEmail(getAttributeValue(attributes.get(userMappings.get(serverKey).getEmailAttribute()))); - return details; + public HttpServletRequest getRequest() { + return request; + } } - } diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapAuthenticatorTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapAuthenticatorTest.java new file mode 100644 index 00000000000..cf56c14eec5 --- /dev/null +++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapAuthenticatorTest.java @@ -0,0 +1,130 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.ldap; + +import org.junit.ClassRule; +import org.junit.Test; +import org.sonar.auth.ldap.server.LdapServer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultLdapAuthenticatorTest { + + /** + * A reference to the original ldif file + */ + public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif"; + /** + * A reference to an aditional ldif file. + */ + public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif"; + @ClassRule + public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF); + @ClassRule + public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com"); + + @Test + public void testNoConnection() { + exampleServer.disableAnonymousAccess(); + try { + LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_SIMPLE).asConfig(), + new LdapAutodiscovery()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); + boolean authenticate = authenticator.authenticate("godin", "secret1"); + assertThat(authenticate).isTrue(); + } finally { + exampleServer.enableAnonymousAccess(); + } + } + + @Test + public void testSimple() { + LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_SIMPLE).asConfig(), + new LdapAutodiscovery()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); + + assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); + assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); + assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); + // SONARPLUGINS-2493 + assertThat(authenticator.authenticate("godin", "")).isFalse(); + assertThat(authenticator.authenticate("godin", null)).isFalse(); + } + + @Test + public void testSimpleMultiLdap() { + LdapSettingsManager settingsManager = new LdapSettingsManager( + LdapSettingsFactory.generateAuthenticationSettings(exampleServer, infosupportServer, LdapContextFactory.AUTH_METHOD_SIMPLE).asConfig(), new LdapAutodiscovery()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); + + assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); + assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); + assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); + // SONARPLUGINS-2493 + assertThat(authenticator.authenticate("godin", "")).isFalse(); + assertThat(authenticator.authenticate("godin", null)).isFalse(); + + // SONARPLUGINS-2793 + assertThat(authenticator.authenticate("robby", "secret1")).isTrue(); + assertThat(authenticator.authenticate("robby", "wrong")).isFalse(); + } + + @Test + public void testSasl() { + LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_CRAM_MD5).asConfig(), + new LdapAutodiscovery()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); + + assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); + assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); + assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); + } + + @Test + public void testSaslMultipleLdap() { + LdapSettingsManager settingsManager = new LdapSettingsManager( + LdapSettingsFactory.generateAuthenticationSettings(exampleServer, infosupportServer, LdapContextFactory.AUTH_METHOD_CRAM_MD5).asConfig(), new LdapAutodiscovery()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); + + assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); + assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); + assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); + + assertThat(authenticator.authenticate("robby", "secret1")).isTrue(); + assertThat(authenticator.authenticate("robby", "wrong")).isFalse(); + } + +} diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapGroupsProviderTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapGroupsProviderTest.java new file mode 100644 index 00000000000..dd759b2804e --- /dev/null +++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapGroupsProviderTest.java @@ -0,0 +1,149 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.ldap; + +import java.util.Collection; +import org.junit.ClassRule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.auth.ldap.server.LdapServer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultLdapGroupsProviderTest { + + /** + * A reference to the original ldif file + */ + public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif"; + /** + * A reference to an aditional ldif file. + */ + public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif"; + + @ClassRule + public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF); + @ClassRule + public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com"); + + @Test + public void defaults() { + MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, null); + + LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + Collection groups; + + groups = groupsProvider.getGroups("tester"); + assertThat(groups).containsOnly("sonar-users"); + + groups = groupsProvider.getGroups("godin"); + assertThat(groups).containsOnly("sonar-users", "sonar-developers"); + + groups = groupsProvider.getGroups("notfound"); + assertThat(groups).isEmpty(); + } + + @Test + public void defaultsMultipleLdap() { + MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); + + LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + + Collection groups; + + groups = groupsProvider.getGroups("tester"); + assertThat(groups).containsOnly("sonar-users"); + + groups = groupsProvider.getGroups("godin"); + assertThat(groups).containsOnly("sonar-users", "sonar-developers"); + + groups = groupsProvider.getGroups("notfound"); + assertThat(groups).isEmpty(); + + groups = groupsProvider.getGroups("testerInfo"); + assertThat(groups).containsOnly("sonar-users"); + + groups = groupsProvider.getGroups("robby"); + assertThat(groups).containsOnly("sonar-users", "sonar-developers"); + } + + @Test + public void posix() { + MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, null); + settings.setProperty("ldap.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))"); + LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + + Collection groups; + + groups = groupsProvider.getGroups("godin"); + assertThat(groups).containsOnly("linux-users"); + } + + @Test + public void posixMultipleLdap() { + MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); + settings.setProperty("ldap.example.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))"); + settings.setProperty("ldap.infosupport.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))"); + LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + + Collection groups; + + groups = groupsProvider.getGroups("godin"); + assertThat(groups).containsOnly("linux-users"); + + groups = groupsProvider.getGroups("robby"); + assertThat(groups).containsOnly("linux-users"); + } + + @Test + public void mixed() { + MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); + settings.setProperty("ldap.example.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))"); + LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + + Collection groups; + + groups = groupsProvider.getGroups("godin"); + assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users"); + } + + @Test + public void mixedMultipleLdap() { + MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); + settings.setProperty("ldap.example.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))"); + settings.setProperty("ldap.infosupport.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))"); + LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + + Collection groups; + + groups = groupsProvider.getGroups("godin"); + assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users"); + + groups = groupsProvider.getGroups("robby"); + assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users"); + } + +} diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapUsersProviderTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapUsersProviderTest.java new file mode 100644 index 00000000000..79f601b520f --- /dev/null +++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapUsersProviderTest.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.auth.ldap; + +import org.junit.ClassRule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.auth.ldap.server.LdapServer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultLdapUsersProviderTest { + /** + * A reference to the original ldif file + */ + public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif"; + /** + * A reference to an aditional ldif file. + */ + public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif"; + + @ClassRule + public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF); + @ClassRule + public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com"); + + @Test + public void test() { + MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); + LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); + DefaultLdapUsersProvider usersProvider = new DefaultLdapUsersProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings()); + + LdapUserDetails details; + + details = usersProvider.getUserDetails("godin"); + assertThat(details.getName()).isEqualTo("Evgeny Mandrikov"); + assertThat(details.getEmail()).isEqualTo("godin@example.org"); + + details = usersProvider.getUserDetails("tester"); + assertThat(details.getName()).isEqualTo("Tester Testerovich"); + assertThat(details.getEmail()).isEqualTo("tester@example.org"); + + details = usersProvider.getUserDetails("without_email"); + assertThat(details.getName()).isEqualTo("Without Email"); + assertThat(details.getEmail()).isEmpty(); + + details = usersProvider.getUserDetails("notfound"); + assertThat(details).isNull(); + + details = usersProvider.getUserDetails("robby"); + assertThat(details.getName()).isEqualTo("Robby Developer"); + assertThat(details.getEmail()).isEqualTo("rd@infosupport.com"); + + details = usersProvider.getUserDetails("testerInfo"); + assertThat(details.getName()).isEqualTo("Tester Testerovich"); + assertThat(details.getEmail()).isEqualTo("tester@infosupport.com"); + } + +} diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java index 71f29891286..ac08ebe08fc 100644 --- a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java +++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java @@ -26,8 +26,6 @@ import org.junit.ClassRule; import org.junit.Test; import org.mockito.Mockito; import org.sonar.api.config.internal.MapSettings; -import org.sonar.api.security.Authenticator; -import org.sonar.api.security.ExternalGroupsProvider; import org.sonar.auth.ldap.server.LdapServer; import static org.assertj.core.api.Assertions.assertThat; @@ -48,13 +46,13 @@ public class KerberosTest { ldapRealm.init(); - assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new Authenticator.Context("Godin@EXAMPLE.ORG", "wrong_user_password", Mockito.mock(HttpServletRequest.class)))) + assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new LdapAuthenticator.Context("Godin@EXAMPLE.ORG", "wrong_user_password", Mockito.mock(HttpServletRequest.class)))) .isFalse(); - assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new Authenticator.Context("Godin@EXAMPLE.ORG", "user_password", Mockito.mock(HttpServletRequest.class)))).isTrue(); + assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new LdapAuthenticator.Context("Godin@EXAMPLE.ORG", "user_password", Mockito.mock(HttpServletRequest.class)))).isTrue(); // Using default realm from krb5.conf: - assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new Authenticator.Context("Godin", "user_password", Mockito.mock(HttpServletRequest.class)))).isTrue(); + assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new LdapAuthenticator.Context("Godin", "user_password", Mockito.mock(HttpServletRequest.class)))).isTrue(); - assertThat(ldapRealm.getGroupsProvider().doGetGroups(new ExternalGroupsProvider.Context("godin", Mockito.mock(HttpServletRequest.class)))).containsOnly("sonar-users"); + assertThat(ldapRealm.getGroupsProvider().doGetGroups(new LdapGroupsProvider.Context("godin", Mockito.mock(HttpServletRequest.class)))).containsOnly("sonar-users"); } @Test diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java deleted file mode 100644 index b02523259f7..00000000000 --- a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.auth.ldap; - -import org.junit.ClassRule; -import org.junit.Test; -import org.sonar.auth.ldap.server.LdapServer; - -import static org.assertj.core.api.Assertions.assertThat; - -public class LdapAuthenticatorTest { - - /** - * A reference to the original ldif file - */ - public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif"; - /** - * A reference to an aditional ldif file. - */ - public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif"; - @ClassRule - public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF); - @ClassRule - public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com"); - - @Test - public void testNoConnection() { - exampleServer.disableAnonymousAccess(); - try { - LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_SIMPLE).asConfig(), - new LdapAutodiscovery()); - LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); - authenticator.authenticate("godin", "secret1"); - } finally { - exampleServer.enableAnonymousAccess(); - } - } - - @Test - public void testSimple() { - LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_SIMPLE).asConfig(), - new LdapAutodiscovery()); - LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); - - assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); - assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); - assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); - // SONARPLUGINS-2493 - assertThat(authenticator.authenticate("godin", "")).isFalse(); - assertThat(authenticator.authenticate("godin", null)).isFalse(); - } - - @Test - public void testSimpleMultiLdap() { - LdapSettingsManager settingsManager = new LdapSettingsManager( - LdapSettingsFactory.generateAuthenticationSettings(exampleServer, infosupportServer, LdapContextFactory.AUTH_METHOD_SIMPLE).asConfig(), new LdapAutodiscovery()); - LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); - - assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); - assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); - assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); - // SONARPLUGINS-2493 - assertThat(authenticator.authenticate("godin", "")).isFalse(); - assertThat(authenticator.authenticate("godin", null)).isFalse(); - - // SONARPLUGINS-2793 - assertThat(authenticator.authenticate("robby", "secret1")).isTrue(); - assertThat(authenticator.authenticate("robby", "wrong")).isFalse(); - } - - @Test - public void testSasl() { - LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_CRAM_MD5).asConfig(), - new LdapAutodiscovery()); - LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); - - assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); - assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); - assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); - } - - @Test - public void testSaslMultipleLdap() { - LdapSettingsManager settingsManager = new LdapSettingsManager( - LdapSettingsFactory.generateAuthenticationSettings(exampleServer, infosupportServer, LdapContextFactory.AUTH_METHOD_CRAM_MD5).asConfig(), new LdapAutodiscovery()); - LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); - - assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); - assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("tester", "secret2")).isTrue(); - assertThat(authenticator.authenticate("tester", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("notfound", "wrong")).isFalse(); - - assertThat(authenticator.authenticate("robby", "secret1")).isTrue(); - assertThat(authenticator.authenticate("robby", "wrong")).isFalse(); - } - -} diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java index 20822a2dbc0..45f8c4c389b 100644 --- a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java +++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java @@ -46,8 +46,6 @@ public class LdapAutoDiscoveryWarningLogTest { MapSettings settings = new MapSettings() .setProperty("ldap.url", server.getUrl()); LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery())); - assertThat(realm.getName()).isEqualTo("LDAP"); - realm.init(); assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty(); @@ -60,7 +58,6 @@ public class LdapAutoDiscoveryWarningLogTest { // ldap.url setting is not set LdapRealm realm = new LdapRealm(new LdapSettingsManager(new MapSettings().setProperty("ldap.realm", "example.org").asConfig(), ldapAutodiscovery)); - realm.init(); assertThat(logTester.logs(LoggerLevel.WARN)).contains("Auto-discovery feature is deprecated, please use 'ldap.url' to specify LDAP url"); diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java deleted file mode 100644 index 8d03958523c..00000000000 --- a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.auth.ldap; - -import java.util.Collection; -import org.junit.ClassRule; -import org.junit.Test; -import org.sonar.api.config.internal.MapSettings; -import org.sonar.auth.ldap.server.LdapServer; - -import static org.assertj.core.api.Assertions.assertThat; - -public class LdapGroupsProviderTest { - - /** - * A reference to the original ldif file - */ - public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif"; - /** - * A reference to an aditional ldif file. - */ - public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif"; - - @ClassRule - public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF); - @ClassRule - public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com"); - - @Test - public void defaults() { - MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, null); - - LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); - Collection groups; - - groups = groupsProvider.getGroups("tester"); - assertThat(groups).containsOnly("sonar-users"); - - groups = groupsProvider.getGroups("godin"); - assertThat(groups).containsOnly("sonar-users", "sonar-developers"); - - groups = groupsProvider.getGroups("notfound"); - assertThat(groups).isEmpty(); - } - - @Test - public void defaultsMultipleLdap() { - MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); - - LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); - - Collection groups; - - groups = groupsProvider.getGroups("tester"); - assertThat(groups).containsOnly("sonar-users"); - - groups = groupsProvider.getGroups("godin"); - assertThat(groups).containsOnly("sonar-users", "sonar-developers"); - - groups = groupsProvider.getGroups("notfound"); - assertThat(groups).isEmpty(); - - groups = groupsProvider.getGroups("testerInfo"); - assertThat(groups).containsOnly("sonar-users"); - - groups = groupsProvider.getGroups("robby"); - assertThat(groups).containsOnly("sonar-users", "sonar-developers"); - } - - @Test - public void posix() { - MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, null); - settings.setProperty("ldap.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))"); - LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); - - Collection groups; - - groups = groupsProvider.getGroups("godin"); - assertThat(groups).containsOnly("linux-users"); - } - - @Test - public void posixMultipleLdap() { - MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); - settings.setProperty("ldap.example.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))"); - settings.setProperty("ldap.infosupport.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))"); - LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); - - Collection groups; - - groups = groupsProvider.getGroups("godin"); - assertThat(groups).containsOnly("linux-users"); - - groups = groupsProvider.getGroups("robby"); - assertThat(groups).containsOnly("linux-users"); - } - - @Test - public void mixed() { - MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); - settings.setProperty("ldap.example.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))"); - LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); - - Collection groups; - - groups = groupsProvider.getGroups("godin"); - assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users"); - } - - @Test - public void mixedMultipleLdap() { - MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); - settings.setProperty("ldap.example.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))"); - settings.setProperty("ldap.infosupport.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))"); - LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); - - Collection groups; - - groups = groupsProvider.getGroups("godin"); - assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users"); - - groups = groupsProvider.getGroups("robby"); - assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users"); - } - -} diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java index 56e9ea07553..ba1656009ef 100644 --- a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java +++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java @@ -24,8 +24,6 @@ import org.junit.ClassRule; import org.junit.Test; import org.mockito.Mockito; import org.sonar.api.config.internal.MapSettings; -import org.sonar.api.security.ExternalGroupsProvider; -import org.sonar.api.security.ExternalUsersProvider; import org.sonar.auth.ldap.server.LdapServer; import static org.assertj.core.api.Assertions.assertThat; @@ -41,10 +39,9 @@ public class LdapRealmTest { MapSettings settings = new MapSettings() .setProperty("ldap.url", server.getUrl()); LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery())); - assertThat(realm.getName()).isEqualTo("LDAP"); realm.init(); - assertThat(realm.doGetAuthenticator()).isInstanceOf(LdapAuthenticator.class); - assertThat(realm.getUsersProvider()).isInstanceOf(ExternalUsersProvider.class).isInstanceOf(LdapUsersProvider.class); + assertThat(realm.doGetAuthenticator()).isInstanceOf(DefaultLdapAuthenticator.class); + assertThat(realm.getUsersProvider()).isInstanceOf(LdapUsersProvider.class).isInstanceOf(DefaultLdapUsersProvider.class); assertThat(realm.getGroupsProvider()).isNull(); } @@ -54,25 +51,26 @@ public class LdapRealmTest { .setProperty("ldap.url", "ldap://no-such-host") .setProperty("ldap.group.baseDn", "cn=groups,dc=example,dc=org"); LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery())); - assertThat(realm.getName()).isEqualTo("LDAP"); try { realm.init(); fail("Since there is no connection, the init method has to throw an exception."); } catch (LdapException e) { assertThat(e).hasMessage("Unable to open LDAP connection"); } - assertThat(realm.doGetAuthenticator()).isInstanceOf(LdapAuthenticator.class); - assertThat(realm.getUsersProvider()).isInstanceOf(ExternalUsersProvider.class).isInstanceOf(LdapUsersProvider.class); - assertThat(realm.getGroupsProvider()).isInstanceOf(ExternalGroupsProvider.class).isInstanceOf(LdapGroupsProvider.class); + assertThat(realm.doGetAuthenticator()).isInstanceOf(DefaultLdapAuthenticator.class); + assertThat(realm.getUsersProvider()).isInstanceOf(LdapUsersProvider.class).isInstanceOf(DefaultLdapUsersProvider.class); + assertThat(realm.getGroupsProvider()).isInstanceOf(LdapGroupsProvider.class).isInstanceOf(DefaultLdapGroupsProvider.class); try { - realm.getUsersProvider().doGetUserDetails(new ExternalUsersProvider.Context("tester", Mockito.mock(HttpServletRequest.class))); + LdapUsersProvider.Context userContext = new DefaultLdapUsersProvider.Context("tester", Mockito.mock(HttpServletRequest.class)); + realm.getUsersProvider().doGetUserDetails(userContext); fail("Since there is no connection, the doGetUserDetails method has to throw an exception."); } catch (LdapException e) { assertThat(e.getMessage()).contains("Unable to retrieve details for user tester"); } try { - realm.getGroupsProvider().doGetGroups(new ExternalGroupsProvider.Context("tester", Mockito.mock(HttpServletRequest.class))); + LdapGroupsProvider.Context groupsContext = new DefaultLdapGroupsProvider.Context("tester", Mockito.mock(HttpServletRequest.class)); + realm.getGroupsProvider().doGetGroups(groupsContext); fail("Since there is no connection, the doGetGroups method has to throw an exception."); } catch (LdapException e) { assertThat(e.getMessage()).contains("Unable to retrieve details for user tester"); diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java deleted file mode 100644 index edbf6e2d1bc..00000000000 --- a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.auth.ldap; - -import org.junit.ClassRule; -import org.junit.Test; -import org.sonar.api.config.internal.MapSettings; -import org.sonar.api.security.UserDetails; -import org.sonar.auth.ldap.server.LdapServer; - -import static org.assertj.core.api.Assertions.assertThat; - -public class LdapUsersProviderTest { - /** - * A reference to the original ldif file - */ - public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif"; - /** - * A reference to an aditional ldif file. - */ - public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif"; - - @ClassRule - public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF); - @ClassRule - public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com"); - - @Test - public void test() { - MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); - LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapUsersProvider usersProvider = new LdapUsersProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings()); - - UserDetails details; - - details = usersProvider.getUserDetails("godin"); - assertThat(details.getName()).isEqualTo("Evgeny Mandrikov"); - assertThat(details.getEmail()).isEqualTo("godin@example.org"); - - details = usersProvider.getUserDetails("tester"); - assertThat(details.getName()).isEqualTo("Tester Testerovich"); - assertThat(details.getEmail()).isEqualTo("tester@example.org"); - - details = usersProvider.getUserDetails("without_email"); - assertThat(details.getName()).isEqualTo("Without Email"); - assertThat(details.getEmail()).isEmpty(); - - details = usersProvider.getUserDetails("notfound"); - assertThat(details).isNull(); - - details = usersProvider.getUserDetails("robby"); - assertThat(details.getName()).isEqualTo("Robby Developer"); - assertThat(details.getEmail()).isEqualTo("rd@infosupport.com"); - - details = usersProvider.getUserDetails("testerInfo"); - assertThat(details.getName()).isEqualTo("Tester Testerovich"); - assertThat(details.getEmail()).isEqualTo("tester@infosupport.com"); - } - -} diff --git a/server/sonar-webserver-auth/build.gradle b/server/sonar-webserver-auth/build.gradle index 70141fa24f5..d12c870ed9c 100644 --- a/server/sonar-webserver-auth/build.gradle +++ b/server/sonar-webserver-auth/build.gradle @@ -17,6 +17,7 @@ dependencies { compile project(':server:sonar-server-common') compile project(':server:sonar-webserver-api') compile project(':sonar-plugin-api-impl') + compile project(':server:sonar-auth-ldap') compile 'org.mindrot:jbcrypt' compileOnly 'com.google.code.findbugs:jsr305' diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java index 5beed48ec0c..781250620a8 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java @@ -33,6 +33,7 @@ public class AuthenticationModule extends Module { BasicAuthentication.class, CredentialsAuthentication.class, CredentialsExternalAuthentication.class, + LdapCredentialsAuthentication.class, CredentialsLocalAuthentication.class, DefaultAdminCredentialsVerifierFilter.class, GithubWebhookAuthentication.class, diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/CredentialsAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/CredentialsAuthentication.java index 3933ba9d251..c2e05801c94 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/CredentialsAuthentication.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/CredentialsAuthentication.java @@ -40,13 +40,16 @@ public class CredentialsAuthentication { private final AuthenticationEvent authenticationEvent; private final CredentialsExternalAuthentication externalAuthentication; private final CredentialsLocalAuthentication localAuthentication; + private final LdapCredentialsAuthentication ldapCredentialsAuthentication; public CredentialsAuthentication(DbClient dbClient, AuthenticationEvent authenticationEvent, - CredentialsExternalAuthentication externalAuthentication, CredentialsLocalAuthentication localAuthentication) { + CredentialsExternalAuthentication externalAuthentication, CredentialsLocalAuthentication localAuthentication, + LdapCredentialsAuthentication ldapCredentialsAuthentication) { this.dbClient = dbClient; this.authenticationEvent = authenticationEvent; this.externalAuthentication = externalAuthentication; this.localAuthentication = localAuthentication; + this.ldapCredentialsAuthentication = ldapCredentialsAuthentication; } public UserDto authenticate(Credentials credentials, HttpServletRequest request, Method method) { @@ -64,7 +67,8 @@ public class CredentialsAuthentication { authenticationEvent.loginSuccess(request, localUser.getLogin(), Source.local(method)); return localUser; } - Optional externalUser = externalAuthentication.authenticate(credentials, request, method); + Optional externalUser = externalAuthentication.authenticate(credentials, request, method) + .or(() -> ldapCredentialsAuthentication.authenticate(credentials, request, method)); if (externalUser.isPresent()) { return externalUser.get(); } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/LdapCredentialsAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/LdapCredentialsAuthentication.java new file mode 100644 index 00000000000..adbbe639ff9 --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/LdapCredentialsAuthentication.java @@ -0,0 +1,184 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.authentication; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Locale; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import org.sonar.api.config.Configuration; +import org.sonar.api.server.authentication.Display; +import org.sonar.api.server.authentication.IdentityProvider; +import org.sonar.api.server.authentication.UserIdentity; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.auth.ldap.LdapAuthenticator; +import org.sonar.auth.ldap.LdapGroupsProvider; +import org.sonar.auth.ldap.LdapRealm; +import org.sonar.auth.ldap.LdapUserDetails; +import org.sonar.auth.ldap.LdapUsersProvider; +import org.sonar.db.user.UserDto; +import org.sonar.process.ProcessProperties; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationEvent.Source; +import org.sonar.server.authentication.event.AuthenticationException; +import org.sonar.server.user.ExternalIdentity; + +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.trimToNull; + +public class LdapCredentialsAuthentication { + + private static final String LDAP_SECURITY_REALM = "LDAP"; + + private static final Logger LOG = Loggers.get(LdapCredentialsAuthentication.class); + + private final Configuration configuration; + private final UserRegistrar userRegistrar; + private final AuthenticationEvent authenticationEvent; + + private final LdapAuthenticator ldapAuthenticator; + private final LdapUsersProvider ldapUsersProvider; + private final LdapGroupsProvider ldapGroupsProvider; + private final boolean isLdapAuthActivated; + + public LdapCredentialsAuthentication(Configuration configuration, + UserRegistrar userRegistrar, AuthenticationEvent authenticationEvent, LdapRealm ldapRealm) { + this.configuration = configuration; + this.userRegistrar = userRegistrar; + this.authenticationEvent = authenticationEvent; + + String realmName = configuration.get(ProcessProperties.Property.SONAR_SECURITY_REALM.getKey()).orElse(null); + this.isLdapAuthActivated = LDAP_SECURITY_REALM.equals(realmName); + + if (isLdapAuthActivated) { + ldapRealm.init(); + this.ldapAuthenticator = ldapRealm.doGetAuthenticator(); + this.ldapUsersProvider = ldapRealm.getUsersProvider(); + this.ldapGroupsProvider = ldapRealm.getGroupsProvider(); + } else { + this.ldapAuthenticator = null; + this.ldapUsersProvider = null; + this.ldapGroupsProvider = null; + } + } + + public Optional authenticate(Credentials credentials, HttpServletRequest request, AuthenticationEvent.Method method) { + if (isLdapAuthActivated) { + return Optional.of(doAuthenticate(fixCase(credentials), request, method)); + } + return Optional.empty(); + } + + private UserDto doAuthenticate(Credentials credentials, HttpServletRequest request, AuthenticationEvent.Method method) { + try { + LdapUsersProvider.Context ldapUsersProviderContext = new LdapUsersProvider.Context(credentials.getLogin(), request); + LdapUserDetails details = ldapUsersProvider.doGetUserDetails(ldapUsersProviderContext); + if (details == null) { + throw AuthenticationException.newBuilder() + .setSource(realmEventSource(method)) + .setLogin(credentials.getLogin()) + .setMessage("No user details") + .build(); + } + LdapAuthenticator.Context ldapAuthenticatorContext = new LdapAuthenticator.Context(credentials.getLogin(), credentials.getPassword().orElse(null), request); + boolean status = ldapAuthenticator.doAuthenticate(ldapAuthenticatorContext); + if (!status) { + throw AuthenticationException.newBuilder() + .setSource(realmEventSource(method)) + .setLogin(credentials.getLogin()) + .setMessage("Realm returned authenticate=false") + .build(); + } + UserDto userDto = synchronize(credentials.getLogin(), details, request, method); + authenticationEvent.loginSuccess(request, credentials.getLogin(), realmEventSource(method)); + return userDto; + } catch (AuthenticationException e) { + throw e; + } catch (Exception e) { + // It seems that with Realm API it's expected to log the error and to not authenticate the user + LOG.error("Error during authentication", e); + throw AuthenticationException.newBuilder() + .setSource(realmEventSource(method)) + .setLogin(credentials.getLogin()) + .setMessage(e.getMessage()) + .build(); + } + } + + private static Source realmEventSource(AuthenticationEvent.Method method) { + return Source.realm(method, "ldap"); + } + + private UserDto synchronize(String userLogin, LdapUserDetails details, HttpServletRequest request, AuthenticationEvent.Method method) { + String name = details.getName(); + UserIdentity.Builder userIdentityBuilder = UserIdentity.builder() + .setName(isEmpty(name) ? userLogin : name) + .setEmail(trimToNull(details.getEmail())) + .setProviderLogin(userLogin); + if (ldapGroupsProvider != null) { + LdapGroupsProvider.Context context = new LdapGroupsProvider.Context(userLogin, request); + Collection groups = ldapGroupsProvider.doGetGroups(context); + userIdentityBuilder.setGroups(new HashSet<>(groups)); + } + return userRegistrar.register( + UserRegistration.builder() + .setUserIdentity(userIdentityBuilder.build()) + .setProvider(new ExternalIdentityProvider()) + .setSource(realmEventSource(method)) + .build()); + } + + private Credentials fixCase(Credentials credentials) { + if (configuration.getBoolean("sonar.authenticator.downcase").orElse(false)) { + return new Credentials(credentials.getLogin().toLowerCase(Locale.ENGLISH), credentials.getPassword().orElse(null)); + } + return credentials; + } + + private static class ExternalIdentityProvider implements IdentityProvider { + @Override + public String getKey() { + return ExternalIdentity.SQ_AUTHORITY; + } + + @Override + public String getName() { + return ExternalIdentity.SQ_AUTHORITY; + } + + @Override + public Display getDisplay() { + return null; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean allowsUsersToSignUp() { + return true; + } + } + +} diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistration.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistration.java index da7cb9b276c..26e27a4e797 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistration.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistration.java @@ -28,7 +28,7 @@ import org.sonar.server.authentication.event.AuthenticationEvent; import static java.util.Objects.requireNonNull; -class UserRegistration { +public class UserRegistration { private final UserIdentity userIdentity; private final IdentityProvider provider; @@ -59,7 +59,7 @@ class UserRegistration { return organizationAlmIds; } - static UserRegistration.Builder builder() { + public static UserRegistration.Builder builder() { return new Builder(); } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SecurityRealmFactory.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SecurityRealmFactory.java index 1330478d0f3..17f50a5ab13 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SecurityRealmFactory.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SecurityRealmFactory.java @@ -21,8 +21,8 @@ package org.sonar.server.user; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; -import org.sonar.api.Startable; import org.sonar.api.CoreProperties; +import org.sonar.api.Startable; import org.sonar.api.config.Configuration; import org.sonar.api.security.LoginPasswordAuthenticator; import org.sonar.api.security.SecurityRealm; @@ -30,10 +30,10 @@ import org.sonar.api.server.ServerSide; import org.sonar.api.utils.SonarException; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; +import org.springframework.beans.factory.annotation.Autowired; import static org.sonar.process.ProcessProperties.Property.SONAR_AUTHENTICATOR_IGNORE_STARTUP_FAILURE; import static org.sonar.process.ProcessProperties.Property.SONAR_SECURITY_REALM; -import org.springframework.beans.factory.annotation.Autowired; /** * @since 2.14 @@ -41,6 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ServerSide public class SecurityRealmFactory implements Startable { + private static final String LDAP_SECURITY_REALM = "LDAP"; private final boolean ignoreStartupFailure; private final SecurityRealm realm; @@ -49,6 +50,12 @@ public class SecurityRealmFactory implements Startable { ignoreStartupFailure = config.getBoolean(SONAR_AUTHENTICATOR_IGNORE_STARTUP_FAILURE.getKey()).orElse(false); String realmName = config.get(SONAR_SECURITY_REALM.getKey()).orElse(null); String className = config.get(CoreProperties.CORE_AUTHENTICATOR_CLASS).orElse(null); + + if (LDAP_SECURITY_REALM.equals(realmName)) { + realm = null; + return; + } + SecurityRealm selectedRealm = null; if (!StringUtils.isEmpty(realmName)) { selectedRealm = selectRealm(realms, realmName); @@ -66,6 +73,7 @@ public class SecurityRealmFactory implements Startable { selectedRealm = new CompatibilityRealm(authenticator); } realm = selectedRealm; + } @Autowired(required = false) diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java index b02bb5fe859..984467262b4 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java @@ -65,7 +65,9 @@ public class CredentialsAuthenticationTest { private final MapSettings settings = new MapSettings().setProperty("sonar.internal.pbkdf2.iterations", NUMBER_OF_PBKDF2_ITERATIONS); private final CredentialsExternalAuthentication externalAuthentication = mock(CredentialsExternalAuthentication.class); private final CredentialsLocalAuthentication localAuthentication = spy(new CredentialsLocalAuthentication(dbClient, settings.asConfig())); - private final CredentialsAuthentication underTest = new CredentialsAuthentication(dbClient, authenticationEvent, externalAuthentication, localAuthentication); + private final LdapCredentialsAuthentication ldapCredentialsAuthentication = mock(LdapCredentialsAuthentication.class); + private final CredentialsAuthentication underTest = new CredentialsAuthentication(dbClient, authenticationEvent, externalAuthentication, localAuthentication, + ldapCredentialsAuthentication); @Test public void authenticate_local_user() { @@ -110,12 +112,31 @@ public class CredentialsAuthenticationTest { executeAuthenticate(BASIC); verify(externalAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC); + verifyNoInteractions(ldapCredentialsAuthentication); verifyNoInteractions(authenticationEvent); } @Test - public void fail_to_authenticate_authenticate_external_user_when_no_external_authentication() { + public void authenticate_ldap_user() { + when(externalAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC)).thenReturn(Optional.empty()); + + String externalId = "12345"; + when(ldapCredentialsAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC)).thenReturn(Optional.of(newUserDto().setExternalId(externalId))); + insertUser(newUserDto() + .setLogin(LOGIN) + .setLocal(false)); + + assertThat(executeAuthenticate(BASIC).getExternalId()).isEqualTo(externalId); + + verify(externalAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC); + verify(ldapCredentialsAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC); + verifyNoInteractions(authenticationEvent); + } + + @Test + public void fail_to_authenticate_external_user_when_no_external_and_ldap_authentication() { when(externalAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN)).thenReturn(Optional.empty()); + when(ldapCredentialsAuthentication.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN)).thenReturn(Optional.empty()); insertUser(newUserDto() .setLogin(LOGIN) .setLocal(false)); @@ -126,6 +147,8 @@ public class CredentialsAuthenticationTest { .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN)) .hasFieldOrPropertyWithValue("login", LOGIN); + verify(externalAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN); + verify(ldapCredentialsAuthentication).authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC_TOKEN); verifyNoInteractions(authenticationEvent); } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java new file mode 100644 index 00000000000..d4ce1980f10 --- /dev/null +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java @@ -0,0 +1,267 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.authentication; + +import javax.servlet.http.HttpServletRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.auth.ldap.LdapAuthenticator; +import org.sonar.auth.ldap.LdapGroupsProvider; +import org.sonar.auth.ldap.LdapRealm; +import org.sonar.auth.ldap.LdapUserDetails; +import org.sonar.auth.ldap.LdapUsersProvider; +import org.sonar.process.ProcessProperties; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationEvent.Source; +import org.sonar.server.authentication.event.AuthenticationException; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class LdapCredentialsAuthenticationTest { + + private static final String LOGIN = "LOGIN"; + private static final String PASSWORD = "PASSWORD"; + + private static final String REALM_NAME = "ldap"; + + private MapSettings settings = new MapSettings(); + private TestUserRegistrar userRegistrar = new TestUserRegistrar(); + + @Mock + private AuthenticationEvent authenticationEvent; + + @Mock + private HttpServletRequest request = mock(HttpServletRequest.class); + + @Mock + private LdapAuthenticator ldapAuthenticator; + @Mock + private LdapGroupsProvider ldapGroupsProvider; + @Mock + private LdapUsersProvider ldapUsersProvider; + @Mock + private LdapRealm ldapRealm; + + private LdapCredentialsAuthentication underTest; + + @Before + public void setUp() throws Exception { + settings.setProperty(ProcessProperties.Property.SONAR_SECURITY_REALM.getKey(), "LDAP"); + when(ldapRealm.doGetAuthenticator()).thenReturn(ldapAuthenticator); + when(ldapRealm.getUsersProvider()).thenReturn(ldapUsersProvider); + when(ldapRealm.getGroupsProvider()).thenReturn(ldapGroupsProvider); + underTest = new LdapCredentialsAuthentication(settings.asConfig(), userRegistrar, authenticationEvent, ldapRealm); + } + + @Test + public void authenticate_with_null_group_provider() { + reset(ldapRealm); + when(ldapRealm.doGetAuthenticator()).thenReturn(ldapAuthenticator); + when(ldapRealm.getUsersProvider()).thenReturn(ldapUsersProvider); + when(ldapRealm.getGroupsProvider()).thenReturn(null); + underTest = new LdapCredentialsAuthentication(settings.asConfig(), userRegistrar, authenticationEvent, ldapRealm); + + when(ldapAuthenticator.doAuthenticate(any(LdapAuthenticator.Context.class))).thenReturn(true); + LdapUserDetails userDetails = new LdapUserDetails(); + userDetails.setName("name"); + userDetails.setEmail("email"); + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(userDetails); + + underTest.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC); + + assertThat(userRegistrar.isAuthenticated()).isTrue(); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().getProviderLogin()).isEqualTo(LOGIN); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().getProviderId()).isNull(); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().getName()).isEqualTo("name"); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().getEmail()).isEqualTo("email"); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().shouldSyncGroups()).isFalse(); + verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME)); + verify(ldapRealm).init(); + } + + @Test + public void authenticate_with_sonarqube_identity_provider() { + when(ldapAuthenticator.doAuthenticate(any(LdapAuthenticator.Context.class))).thenReturn(true); + LdapUserDetails userDetails = new LdapUserDetails(); + userDetails.setName("name"); + userDetails.setEmail("email"); + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(userDetails); + + underTest.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC); + + assertThat(userRegistrar.isAuthenticated()).isTrue(); + assertThat(userRegistrar.getAuthenticatorParameters().getProvider().getKey()).isEqualTo("sonarqube"); + assertThat(userRegistrar.getAuthenticatorParameters().getProvider().getName()).isEqualTo("sonarqube"); + assertThat(userRegistrar.getAuthenticatorParameters().getProvider().getDisplay()).isNull(); + assertThat(userRegistrar.getAuthenticatorParameters().getProvider().isEnabled()).isTrue(); + verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME)); + verify(ldapRealm).init(); + } + + @Test + public void login_is_used_when_no_name_provided() { + when(ldapAuthenticator.doAuthenticate(any(LdapAuthenticator.Context.class))).thenReturn(true); + LdapUserDetails userDetails = new LdapUserDetails(); + userDetails.setEmail("email"); + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(userDetails); + + underTest.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC); + + assertThat(userRegistrar.getAuthenticatorParameters().getProvider().getName()).isEqualTo("sonarqube"); + verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME)); + } + + @Test + public void authenticate_with_group_sync() { + when(ldapGroupsProvider.doGetGroups(any(LdapGroupsProvider.Context.class))).thenReturn(asList("group1", "group2")); + + executeAuthenticate(); + + assertThat(userRegistrar.isAuthenticated()).isTrue(); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().shouldSyncGroups()).isTrue(); + verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME)); + } + + @Test + public void use_login_if_user_details_contains_no_name() { + when(ldapAuthenticator.doAuthenticate(any(LdapAuthenticator.Context.class))).thenReturn(true); + LdapUserDetails userDetails = new LdapUserDetails(); + userDetails.setName(null); + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(userDetails); + + underTest.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC); + + assertThat(userRegistrar.isAuthenticated()).isTrue(); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().getName()).isEqualTo(LOGIN); + verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME)); + } + + @Test + public void use_downcase_login() { + settings.setProperty("sonar.authenticator.downcase", true); + + executeAuthenticate("LOGIN"); + + assertThat(userRegistrar.isAuthenticated()).isTrue(); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().getProviderLogin()).isEqualTo("login"); + verify(authenticationEvent).loginSuccess(request, "login", Source.realm(BASIC, REALM_NAME)); + } + + @Test + public void does_not_user_downcase_login() { + settings.setProperty("sonar.authenticator.downcase", false); + + executeAuthenticate("LoGiN"); + + assertThat(userRegistrar.isAuthenticated()).isTrue(); + assertThat(userRegistrar.getAuthenticatorParameters().getUserIdentity().getProviderLogin()).isEqualTo("LoGiN"); + verify(authenticationEvent).loginSuccess(request, "LoGiN", Source.realm(BASIC, REALM_NAME)); + } + + @Test + public void fail_to_authenticate_when_user_details_are_null() { + when(ldapAuthenticator.doAuthenticate(any(LdapAuthenticator.Context.class))).thenReturn(true); + + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(null); + + Credentials credentials = new Credentials(LOGIN, PASSWORD); + assertThatThrownBy(() -> underTest.authenticate(credentials, request, BASIC)) + .hasMessage("No user details") + .isInstanceOf(AuthenticationException.class) + .hasFieldOrPropertyWithValue("source", Source.realm(BASIC, REALM_NAME)) + .hasFieldOrPropertyWithValue("login", LOGIN); + + verifyNoInteractions(authenticationEvent); + } + + @Test + public void fail_to_authenticate_when_external_authentication_fails() { + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(new LdapUserDetails()); + + when(ldapAuthenticator.doAuthenticate(any(LdapAuthenticator.Context.class))).thenReturn(false); + + Credentials credentials = new Credentials(LOGIN, PASSWORD); + assertThatThrownBy(() -> underTest.authenticate(credentials, request, BASIC)) + .hasMessage("Realm returned authenticate=false") + .isInstanceOf(AuthenticationException.class) + .hasFieldOrPropertyWithValue("source", Source.realm(BASIC, REALM_NAME)) + .hasFieldOrPropertyWithValue("login", LOGIN); + + verifyNoInteractions(authenticationEvent); + + } + + @Test + public void fail_to_authenticate_when_any_exception_is_thrown() { + String expectedMessage = "emulating exception in doAuthenticate"; + doThrow(new IllegalArgumentException(expectedMessage)).when(ldapAuthenticator).doAuthenticate(any(LdapAuthenticator.Context.class)); + + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(new LdapUserDetails()); + + Credentials credentials = new Credentials(LOGIN, PASSWORD); + assertThatThrownBy(() -> underTest.authenticate(credentials, request, BASIC_TOKEN)) + .hasMessage(expectedMessage) + .isInstanceOf(AuthenticationException.class) + .hasFieldOrPropertyWithValue("source", Source.realm(BASIC_TOKEN, REALM_NAME)) + .hasFieldOrPropertyWithValue("login", LOGIN); + + verifyNoInteractions(authenticationEvent); + } + + @Test + public void return_empty_user_when_ldap_not_activated() { + reset(ldapRealm); + settings.clear(); + underTest = new LdapCredentialsAuthentication(settings.asConfig(), userRegistrar, authenticationEvent, ldapRealm); + + assertThat(underTest.authenticate(new Credentials(LOGIN, PASSWORD), request, BASIC)).isEmpty(); + verifyNoInteractions(authenticationEvent); + verifyNoInteractions(ldapRealm); + } + + private void executeAuthenticate() { + executeAuthenticate(LOGIN); + } + + private void executeAuthenticate(String login) { + when(ldapAuthenticator.doAuthenticate(any(LdapAuthenticator.Context.class))).thenReturn(true); + LdapUserDetails userDetails = new LdapUserDetails(); + userDetails.setName("name"); + when(ldapUsersProvider.doGetUserDetails(any(LdapUsersProvider.Context.class))).thenReturn(userDetails); + underTest.authenticate(new Credentials(login, PASSWORD), request, BASIC); + } + +} diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SecurityRealmFactoryTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SecurityRealmFactoryTest.java index e88aad033da..2b8065c44e2 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SecurityRealmFactoryTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SecurityRealmFactoryTest.java @@ -60,6 +60,15 @@ public class SecurityRealmFactoryTest { assertThat(factory.hasExternalAuthentication()).isFalse(); } + @Test + public void return_null_if_realm_is_ldap() { + settings.setProperty("sonar.security.realm", "LDAP"); + SecurityRealmFactory factory = new SecurityRealmFactory(settings.asConfig()); + factory.start(); + assertThat(factory.getRealm()).isNull(); + assertThat(factory.hasExternalAuthentication()).isFalse(); + } + @Test public void realm_not_found() { settings.setProperty("sonar.security.realm", "Fake"); -- cgit v1.2.3