aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorAurelien Poscia <aurelien.poscia@sonarsource.com>2022-10-25 12:03:35 +0200
committersonartech <sonartech@sonarsource.com>2022-11-02 20:03:01 +0000
commita22087da3ca968ed5a592ed03d47f282b6b59d63 (patch)
tree65c49f19b92f2054d8c7033222bad336c1d212c3 /server
parentb037cda5210fda8a03d45c89212037f666085044 (diff)
downloadsonarqube-a22087da3ca968ed5a592ed03d47f282b6b59d63.tar.gz
sonarqube-a22087da3ca968ed5a592ed03d47f282b6b59d63.zip
SONAR-17508 Decouple LDAP authentication from sonar-plugin-API
Diffstat (limited to 'server')
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapAuthenticator.java145
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapGroupsProvider.java139
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/DefaultLdapUsersProvider.java120
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java127
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java124
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java29
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserDetails.java75
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java107
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapAuthenticatorTest.java (renamed from server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java)15
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapGroupsProviderTest.java (renamed from server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java)14
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/DefaultLdapUsersProviderTest.java (renamed from server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java)7
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java10
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java3
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java20
-rw-r--r--server/sonar-webserver-auth/build.gradle1
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java1
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/CredentialsAuthentication.java8
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/LdapCredentialsAuthentication.java184
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserRegistration.java4
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/user/SecurityRealmFactory.java12
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/CredentialsAuthenticationTest.java27
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/LdapCredentialsAuthenticationTest.java267
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/user/SecurityRealmFactoryTest.java9
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");