diff options
Diffstat (limited to 'server')
23 files changed, 1082 insertions, 366 deletions
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<String, LdapContextFactory> contextFactories; + private final Map<String, LdapUserMapping> userMappings; + + public DefaultLdapAuthenticator(Map<String, LdapContextFactory> contextFactories, Map<String, LdapUserMapping> 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<String, LdapUserMapping> 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<String, LdapContextFactory> contextFactories; + private final Map<String, LdapUserMapping> userMappings; + private final Map<String, LdapGroupMapping> groupMappings; + + public DefaultLdapGroupsProvider(Map<String, LdapContextFactory> contextFactories, Map<String, LdapUserMapping> userMappings, Map<String, LdapGroupMapping> groupMapping) { + this.contextFactories = contextFactories; + this.userMappings = userMappings; + this.groupMappings = groupMapping; + } + + @Override + public Collection<String> doGetGroups(Context context) { + return getGroups(context.getUsername()); + } + + /** + * @throws LdapException if unable to retrieve groups + */ + public Collection<String> getGroups(String username) { + checkPrerequisites(username); + Set<String> groups = new HashSet<>(); + List<LdapException> exceptions = new ArrayList<>(); + for (String serverKey : userMappings.keySet()) { + if (groupMappings.containsKey(serverKey)) { + SearchResult searchResult = searchUserGroups(username, exceptions, serverKey); + if (searchResult != null) { + try { + NamingEnumeration<SearchResult> 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<String> groups, List<LdapException> 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<LdapException> 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<String> mapGroups(String serverKey, NamingEnumeration<SearchResult> searchResult) throws NamingException { + Set<String> 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<String, LdapContextFactory> contextFactories; + private final Map<String, LdapUserMapping> userMappings; + + public DefaultLdapUsersProvider(Map<String, LdapContextFactory> contextFactories, Map<String, LdapUserMapping> 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<String, LdapUserMapping> 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<String, LdapContextFactory> contextFactories; - private final Map<String, LdapUserMapping> userMappings; - - public LdapAuthenticator(Map<String, LdapContextFactory> contextFactories, Map<String, LdapUserMapping> 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 <a href="http://www.jasig.org/cas">CAS</a>. + */ + 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 <a href="http://www.jasig.org/cas">CAS</a>. + */ + 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<String> doGetGroups(Context context); - private final Map<String, LdapContextFactory> contextFactories; - private final Map<String, LdapUserMapping> userMappings; - private final Map<String, LdapGroupMapping> groupMappings; + final class Context { + private String username; + private HttpServletRequest request; - public LdapGroupsProvider(Map<String, LdapContextFactory> contextFactories, Map<String, LdapUserMapping> userMappings, Map<String, LdapGroupMapping> groupMapping) { - this.contextFactories = contextFactories; - this.userMappings = userMappings; - this.groupMappings = groupMapping; - } - - @Override - public Collection<String> doGetGroups(Context context) { - return getGroups(context.getUsername()); - } - - /** - * @throws LdapException if unable to retrieve groups - */ - public Collection<String> getGroups(String username) { - checkPrerequisites(username); - Set<String> groups = new HashSet<>(); - List<LdapException> 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<SearchResult> 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<String> groups, List<LdapException> 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<LdapException> 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<String> mapGroups(String serverKey, NamingEnumeration<SearchResult> searchResult) throws NamingException { - Set<String> 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<String, LdapContextFactory> contextFactories = settingsManager.getContextFactories(); Map<String, LdapUserMapping> userMappings = settingsManager.getUserMappings(); - usersProvider = new LdapUsersProvider(contextFactories, userMappings); - authenticator = new LdapAuthenticator(contextFactories, userMappings); + usersProvider = new DefaultLdapUsersProvider(contextFactories, userMappings); + authenticator = new DefaultLdapAuthenticator(contextFactories, userMappings); Map<String, LdapGroupMapping> 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<String, LdapContextFactory> contextFactories; - private final Map<String, LdapUserMapping> userMappings; + LdapUserDetails doGetUserDetails(Context context); - public LdapUsersProvider(Map<String, LdapContextFactory> contextFactories, Map<String, LdapUserMapping> 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/LdapAuthenticatorTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapAuthenticatorTest.java index b02523259f7..cf56c14eec5 100644 --- 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/DefaultLdapAuthenticatorTest.java @@ -25,7 +25,7 @@ import org.sonar.auth.ldap.server.LdapServer; import static org.assertj.core.api.Assertions.assertThat; -public class LdapAuthenticatorTest { +public class DefaultLdapAuthenticatorTest { /** * A reference to the original ldif file @@ -46,8 +46,9 @@ public class LdapAuthenticatorTest { 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"); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); + boolean authenticate = authenticator.authenticate("godin", "secret1"); + assertThat(authenticate).isTrue(); } finally { exampleServer.enableAnonymousAccess(); } @@ -57,7 +58,7 @@ public class LdapAuthenticatorTest { 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()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); @@ -75,7 +76,7 @@ public class LdapAuthenticatorTest { 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()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); @@ -97,7 +98,7 @@ public class LdapAuthenticatorTest { 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()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); @@ -112,7 +113,7 @@ public class LdapAuthenticatorTest { 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()); + DefaultLdapAuthenticator authenticator = new DefaultLdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings()); assertThat(authenticator.authenticate("godin", "secret1")).isTrue(); assertThat(authenticator.authenticate("godin", "wrong")).isFalse(); 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/DefaultLdapGroupsProviderTest.java index 8d03958523c..dd759b2804e 100644 --- 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/DefaultLdapGroupsProviderTest.java @@ -27,7 +27,7 @@ import org.sonar.auth.ldap.server.LdapServer; import static org.assertj.core.api.Assertions.assertThat; -public class LdapGroupsProviderTest { +public class DefaultLdapGroupsProviderTest { /** * A reference to the original ldif file @@ -48,7 +48,7 @@ public class LdapGroupsProviderTest { MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, null); LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); Collection<String> groups; groups = groupsProvider.getGroups("tester"); @@ -66,7 +66,7 @@ public class LdapGroupsProviderTest { MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer); LdapSettingsManager settingsManager = new LdapSettingsManager(settings.asConfig(), new LdapAutodiscovery()); - LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); Collection<String> groups; @@ -91,7 +91,7 @@ public class LdapGroupsProviderTest { 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()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); Collection<String> groups; @@ -105,7 +105,7 @@ public class LdapGroupsProviderTest { 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()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); Collection<String> groups; @@ -121,7 +121,7 @@ public class LdapGroupsProviderTest { 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()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); Collection<String> groups; @@ -135,7 +135,7 @@ public class LdapGroupsProviderTest { 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()); + DefaultLdapGroupsProvider groupsProvider = new DefaultLdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings()); Collection<String> groups; 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/DefaultLdapUsersProviderTest.java index edbf6e2d1bc..79f601b520f 100644 --- 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/DefaultLdapUsersProviderTest.java @@ -22,12 +22,11 @@ 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 { +public class DefaultLdapUsersProviderTest { /** * A reference to the original ldif file */ @@ -46,9 +45,9 @@ public class LdapUsersProviderTest { 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()); + DefaultLdapUsersProvider usersProvider = new DefaultLdapUsersProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings()); - UserDetails details; + LdapUserDetails details; details = usersProvider.getUserDetails("godin"); assertThat(details.getName()).isEqualTo("Evgeny Mandrikov"); 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/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/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-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<UserDto> externalUser = externalAuthentication.authenticate(credentials, request, method); + Optional<UserDto> 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<UserDto> 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<String> 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 @@ -61,6 +61,15 @@ public class SecurityRealmFactoryTest { } @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"); |