aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.cirrus.yml37
-rw-r--r--server/sonar-auth-ldap/build.gradle22
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/CallbackHandlerImpl.java57
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/ContextHelper.java82
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/Krb5LoginConfiguration.java61
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java130
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAutodiscovery.java165
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapContextFactory.java247
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapException.java32
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupMapping.java152
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java144
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapModule.java34
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java82
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSearch.java191
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSettingsManager.java195
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserMapping.java135
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java125
-rw-r--r--server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/package-info.java23
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/CallbackHandlerImplTest.java48
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/ContextHelperTest.java53
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java84
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java129
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java100
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutodiscoveryTest.java93
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupMappingTest.java65
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java149
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapModuleTest.java38
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java82
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapReferralsTest.java69
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSearchTest.java118
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsFactory.java95
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsManagerTest.java201
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUserMappingTest.java76
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java77
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/ApacheDS.java234
-rw-r--r--server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/LdapServer.java68
-rw-r--r--server/sonar-auth-ldap/src/test/resources/conf/krb5.conf20
-rw-r--r--server/sonar-auth-ldap/src/test/resources/conf/sasl_mech.properties23
-rw-r--r--server/sonar-auth-ldap/src/test/resources/krb.ldif55
-rw-r--r--server/sonar-auth-ldap/src/test/resources/logback-test.xml45
-rw-r--r--server/sonar-auth-ldap/src/test/resources/static-groups.example.org.ldif81
-rw-r--r--server/sonar-auth-ldap/src/test/resources/users-apacheds.ldif88
-rw-r--r--server/sonar-auth-ldap/src/test/resources/users.example.org.ldif98
-rw-r--r--server/sonar-auth-ldap/src/test/resources/users.infosupport.com.ldif98
-rw-r--r--server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java2
-rw-r--r--server/sonar-docs/src/pages/instance-administration/delegated-auth.md2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java15
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java21
-rw-r--r--server/sonar-webserver/build.gradle1
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java2
-rw-r--r--settings.gradle2
-rw-r--r--sonar-application/build.gradle1
-rw-r--r--sonar-testing-ldap/build.gradle15
-rw-r--r--sonar-testing-ldap/src/main/java/org/sonar/ldap/ApacheDS.java240
-rw-r--r--sonar-testing-ldap/src/main/java/org/sonar/ldap/package-info.java20
-rw-r--r--sonar-testing-ldap/src/test/java/org/sonar/ldap/ApacheDSTest.java38
-rw-r--r--sonar-testing-ldap/src/test/resources/change.ldif5
-rw-r--r--sonar-testing-ldap/src/test/resources/delete.ldif2
-rw-r--r--sonar-testing-ldap/src/test/resources/init.ldif9
-rw-r--r--sonar-testing-ldap/src/test/resources/logback-test.xml41
60 files changed, 4603 insertions, 14 deletions
diff --git a/.cirrus.yml b/.cirrus.yml
index fca399b5ecb..88a4e9e6c8b 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -211,12 +211,49 @@ qa_saml_task:
path: "**/test-results/**/*.xml"
format: junit
+# LDAP QA is executed in a dedicated task in order to not slow down the pipeline, as a LDAP server and SonarQube server are re-started on each test.
+qa_ldap_task:
+ depends_on: build
+ # Comment the following line and commit with message "DO NOT MERGE" in order to run
+ # this task on your branch
+ only_if: $CIRRUS_BRANCH == "branch-nightly-build"
+ gke_container:
+ dockerfile: private/docker/Dockerfile-build
+ builder_image_project: ci-cd-215716
+ builder_image_name: docker-builder-v1
+ cluster_name: cirrus-uscentral1a-cluster
+ zone: us-central1-a
+ namespace: default
+ cpu: 2.4
+ memory: 10Gb
+ env:
+ # No need to clone the full history.
+ # Depth of 1 is not enough because it would fail the build in case of consecutive pushes
+ # (example of error: "Hard resetting to c968ecaf7a1942dacecd78480b3751ac74d53c33...Failed to force reset to c968ecaf7a1942dacecd78480b3751ac74d53c33: object not found!")
+ CIRRUS_CLONE_DEPTH: 50
+ QA_CATEGORY: LDAP
+ gradle_cache:
+ folder: ~/.gradle/caches
+ script:
+ - ./private/cirrus/cirrus-qa.sh h2
+ cleanup_before_cache_script:
+ - ./private/cirrus/cleanup-gradle-cache.sh
+ on_failure:
+ reports_artifacts:
+ path: "**/build/reports/**/*"
+ screenshots_artifacts:
+ path: "**/build/screenshots/**/*"
+ junit_artifacts:
+ path: "**/test-results/**/*.xml"
+ format: junit
+
promote_task:
depends_on:
- build
- validate
- qa
- qa_saml
+ - qa_ldap
only_if: $CIRRUS_BRANCH !=~ "dogfood/.*" && $CIRRUS_BRANCH != "public_master" && $CIRRUS_BRANCH != "branch-nightly-build"
gke_container:
dockerfile: private/docker/Dockerfile-build
diff --git a/server/sonar-auth-ldap/build.gradle b/server/sonar-auth-ldap/build.gradle
new file mode 100644
index 00000000000..622b983188a
--- /dev/null
+++ b/server/sonar-auth-ldap/build.gradle
@@ -0,0 +1,22 @@
+description = 'SonarQube :: Authentication :: LDAP'
+
+configurations {
+ testCompile.extendsFrom compileOnly
+}
+
+dependencies {
+ // please keep the list ordered
+
+ compile 'commons-lang:commons-lang'
+
+ compileOnly 'com.google.code.findbugs:jsr305'
+ compileOnly 'javax.servlet:javax.servlet-api'
+ compileOnly project(':sonar-core')
+
+ testCompile 'com.tngtech.java:junit-dataprovider'
+ testCompile 'junit:junit'
+ testCompile 'org.assertj:assertj-core'
+ testCompile 'org.mockito:mockito-core'
+ testCompile project(":sonar-testing-ldap")
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/CallbackHandlerImpl.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/CallbackHandlerImpl.java
new file mode 100644
index 00000000000..a5760d379d3
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/CallbackHandlerImpl.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.io.IOException;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public class CallbackHandlerImpl implements CallbackHandler {
+ private String name;
+ private String password;
+
+ public CallbackHandlerImpl(String name, String password) {
+ this.name = name;
+ this.password = password;
+ }
+
+ @Override
+ public void handle(Callback[] callbacks) throws UnsupportedCallbackException, IOException {
+ for (Callback callBack : callbacks) {
+ if (callBack instanceof NameCallback) {
+ // Handles username callback
+ NameCallback nameCallback = (NameCallback) callBack;
+ nameCallback.setName(name);
+ } else if (callBack instanceof PasswordCallback) {
+ // Handles password callback
+ PasswordCallback passwordCallback = (PasswordCallback) callBack;
+ passwordCallback.setPassword(password.toCharArray());
+ } else {
+ throw new UnsupportedCallbackException(callBack, "Callback not supported");
+ }
+ }
+ }
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/ContextHelper.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/ContextHelper.java
new file mode 100644
index 00000000000..ca02f0e7aae
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/ContextHelper.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 javax.annotation.Nullable;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public final class ContextHelper {
+
+ private static final Logger LOG = Loggers.get(ContextHelper.class);
+
+ private ContextHelper() {
+ }
+
+ /**
+ * <pre>
+ * public void useContextNicely() throws NamingException {
+ * InitialDirContext context = null;
+ * boolean threw = true;
+ * try {
+ * context = new InitialDirContext();
+ * // Some code which does something with the Context and may throw a NamingException
+ * threw = false; // No throwable thrown
+ * } finally {
+ * // Close context
+ * // If an exception occurs, only rethrow it if (threw==false)
+ * close(context, threw);
+ * }
+ * }
+ * </pre>
+ *
+ * @param context the {@code Context} object to be closed, or null, in which case this method does nothing
+ * @param swallowIOException if true, don't propagate {@code NamingException} thrown by the {@code close} method
+ * @throws NamingException if {@code swallowIOException} is false and {@code close} throws a {@code NamingException}.
+ */
+ public static void close(@Nullable Context context, boolean swallowIOException) throws NamingException {
+ if (context == null) {
+ return;
+ }
+ try {
+ context.close();
+ } catch (NamingException e) {
+ if (swallowIOException) {
+ LOG.warn("NamingException thrown while closing context.", e);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ public static void closeQuietly(@Nullable Context context) {
+ try {
+ close(context, true);
+ } catch (NamingException e) {
+ LOG.error("Unexpected NamingException", e);
+ }
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/Krb5LoginConfiguration.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/Krb5LoginConfiguration.java
new file mode 100644
index 00000000000..c13360733c6
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/Krb5LoginConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.HashMap;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public class Krb5LoginConfiguration extends Configuration {
+ private static final AppConfigurationEntry[] CONFIG_LIST = new AppConfigurationEntry[1];
+
+ static {
+ String loginModule = "com.sun.security.auth.module.Krb5LoginModule";
+ AppConfigurationEntry.LoginModuleControlFlag flag = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED;
+ CONFIG_LIST[0] = new AppConfigurationEntry(loginModule, flag, new HashMap<String, Object>());
+ }
+
+ /**
+ * Creates a new instance of Krb5LoginConfiguration.
+ */
+ public Krb5LoginConfiguration() {
+ super();
+ }
+
+ /**
+ * Interface method requiring us to return all the LoginModules we know about.
+ */
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String applicationName) {
+ // We will ignore the applicationName, since we want all apps to use Kerberos V5
+ return CONFIG_LIST.clone();
+ }
+
+ /**
+ * Interface method for reloading the configuration. We don't need this.
+ */
+ @Override
+ public void refresh() {
+ // Right now this is a load once scheme and we will not implement the refresh method
+ }
+}
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
new file mode 100644
index 00000000000..01f1be3e941
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java
@@ -0,0 +1,130 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.security.Authenticator;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+/**
+ * @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;
+ }
+
+ @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 (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;
+ }
+
+ 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/LdapAutodiscovery.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAutodiscovery.java
new file mode 100644
index 00000000000..3e9254a60f8
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAutodiscovery.java
@@ -0,0 +1,165 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import org.apache.commons.lang.math.NumberUtils;
+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 LdapAutodiscovery {
+
+ private static final Logger LOG = Loggers.get(LdapAutodiscovery.class);
+
+ /**
+ * Get the DNS domain name (eg: example.org).
+ *
+ * @return DNS domain
+ * @throws java.net.UnknownHostException if unable to determine DNS domain
+ */
+ public static String getDnsDomainName() throws UnknownHostException {
+ return getDnsDomainName(InetAddress.getLocalHost().getCanonicalHostName());
+ }
+
+ /**
+ * Extracts DNS domain name from Fully Qualified Domain Name.
+ *
+ * @param fqdn Fully Qualified Domain Name
+ * @return DNS domain name or null, if can't be extracted
+ */
+ public static String getDnsDomainName(String fqdn) {
+ if (fqdn.indexOf('.') == -1) {
+ return null;
+ }
+ return fqdn.substring(fqdn.indexOf('.') + 1);
+ }
+
+ /**
+ * Get the DNS DN domain (eg: dc=example,dc=org).
+ *
+ * @param domain DNS domain
+ * @return DNS DN domain
+ */
+ public static String getDnsDomainDn(String domain) {
+ StringBuilder result = new StringBuilder();
+ String[] domainPart = domain.split("[.]");
+ for (int i = 0; i < domainPart.length; i++) {
+ result.append(i > 0 ? "," : "").append("dc=").append(domainPart[i]);
+ }
+ return result.toString();
+ }
+
+ /**
+ * Get LDAP server(s) from DNS.
+ *
+ * @param domain DNS domain
+ * @return LDAP server(s) or empty if unable to determine
+ */
+ public List<LdapSrvRecord> getLdapServers(String domain) {
+ try {
+ return getLdapServers(new InitialDirContext(), domain);
+ } catch (NamingException e) {
+ LOG.error("Unable to determine LDAP server(s) from DNS", e);
+ return Collections.emptyList();
+ }
+ }
+
+ List<LdapSrvRecord> getLdapServers(DirContext context, String domain) throws NamingException {
+ Attributes lSrvAttrs = context.getAttributes("dns:/_ldap._tcp." + domain, new String[] {"srv"});
+ Attribute serversAttribute = lSrvAttrs.get("srv");
+ NamingEnumeration<?> lEnum = serversAttribute.getAll();
+ SortedSet<LdapSrvRecord> result = new TreeSet<>();
+ while (lEnum.hasMore()) {
+ String srvRecord = (String) lEnum.next();
+ // priority weight port target
+ String[] srvData = srvRecord.split(" ");
+
+ int priority = NumberUtils.toInt(srvData[0]);
+ int weight = NumberUtils.toInt(srvData[1]);
+ String port = srvData[2];
+ String target = srvData[3];
+
+ if (target.endsWith(".")) {
+ target = target.substring(0, target.length() - 1);
+ }
+ String server = "ldap://" + target + ":" + port;
+ result.add(new LdapSrvRecord(server, priority, weight));
+ }
+ return new ArrayList<>(result);
+ }
+
+ public static class LdapSrvRecord implements Comparable<LdapSrvRecord> {
+ private final String serverUrl;
+ private final int priority;
+ private final int weight;
+
+ public LdapSrvRecord(String serverUrl, int priority, int weight) {
+ this.serverUrl = serverUrl;
+ this.priority = priority;
+ this.weight = weight;
+ }
+
+ @Override
+ public int compareTo(LdapSrvRecord o) {
+ if (this.priority == o.priority) {
+ return Integer.compare(o.weight, this.weight);
+ }
+ return Integer.compare(this.priority, o.priority);
+ }
+
+ String getServerUrl() {
+ return serverUrl;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ return this.serverUrl.equals(((LdapSrvRecord) obj).serverUrl);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.serverUrl.hashCode();
+ }
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapContextFactory.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapContextFactory.java
new file mode 100644
index 00000000000..405ccd72c3a
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapContextFactory.java
@@ -0,0 +1,247 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.io.IOException;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Properties;
+import javax.annotation.Nullable;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.StartTlsRequest;
+import javax.naming.ldap.StartTlsResponse;
+import javax.security.auth.Subject;
+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.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public class LdapContextFactory {
+
+ private static final Logger LOG = Loggers.get(LdapContextFactory.class);
+
+ // visible for testing
+ static final String AUTH_METHOD_SIMPLE = "simple";
+ static final String AUTH_METHOD_GSSAPI = "GSSAPI";
+ static final String AUTH_METHOD_DIGEST_MD5 = "DIGEST-MD5";
+ static final String AUTH_METHOD_CRAM_MD5 = "CRAM-MD5";
+
+ private static final String REFERRALS_FOLLOW_MODE = "follow";
+ private static final String REFERRALS_IGNORE_MODE = "ignore";
+
+ private static final String DEFAULT_AUTHENTICATION = AUTH_METHOD_SIMPLE;
+ private static final String DEFAULT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
+
+ /**
+ * The Sun LDAP property used to enable connection pooling. This is used in the default implementation to enable
+ * LDAP connection pooling.
+ */
+ private static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
+
+ private static final String SASL_REALM_PROPERTY = "java.naming.security.sasl.realm";
+
+ private final String providerUrl;
+ private final boolean startTLS;
+ private final String authentication;
+ private final String factory;
+ private final String username;
+ private final String password;
+ private final String realm;
+ private final String referral;
+
+ public LdapContextFactory(Settings settings, String settingsPrefix, String ldapUrl) {
+ this.authentication = StringUtils.defaultString(settings.getString(settingsPrefix + ".authentication"), DEFAULT_AUTHENTICATION);
+ this.factory = StringUtils.defaultString(settings.getString(settingsPrefix + ".contextFactoryClass"), DEFAULT_FACTORY);
+ this.realm = settings.getString(settingsPrefix + ".realm");
+ this.providerUrl = ldapUrl;
+ this.startTLS = settings.getBoolean(settingsPrefix + ".StartTLS");
+ this.username = settings.getString(settingsPrefix + ".bindDn");
+ this.password = settings.getString(settingsPrefix + ".bindPassword");
+ this.referral = getReferralsMode(settings, settingsPrefix + ".followReferrals");
+ }
+
+ /**
+ * Returns {@code InitialDirContext} for Bind user.
+ */
+ public InitialDirContext createBindContext() throws NamingException {
+ if (isGssapi()) {
+ return createInitialDirContextUsingGssapi(username, password);
+ } else {
+ return createInitialDirContext(username, password, true);
+ }
+ }
+
+ /**
+ * Returns {@code InitialDirContext} for specified user.
+ * Note that pooling intentionally disabled by this method.
+ */
+ public InitialDirContext createUserContext(String principal, String credentials) throws NamingException {
+ return createInitialDirContext(principal, credentials, false);
+ }
+
+ private InitialDirContext createInitialDirContext(String principal, String credentials, boolean pooling) throws NamingException {
+ final InitialLdapContext ctx;
+ if (startTLS) {
+ // Note that pooling is not enabled for such connections, because "Stop TLS" is not performed.
+ Properties env = new Properties();
+ env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
+ env.put(Context.PROVIDER_URL, providerUrl);
+ env.put(Context.REFERRAL, referral);
+ // At this point env should not contain properties SECURITY_AUTHENTICATION, SECURITY_PRINCIPAL and SECURITY_CREDENTIALS to avoid
+ // "bind" operation prior to StartTLS:
+ ctx = new InitialLdapContext(env, null);
+ // http://docs.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html
+ StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
+ try {
+ tls.negotiate();
+ } catch (IOException e) {
+ NamingException ex = new NamingException("StartTLS failed");
+ ex.initCause(e);
+ throw ex;
+ }
+ // Explicitly initiate "bind" operation:
+ ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, authentication);
+ if (principal != null) {
+ ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal);
+ }
+ if (credentials != null) {
+ ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
+ }
+ ctx.reconnect(null);
+ } else {
+ ctx = new InitialLdapContext(getEnvironment(principal, credentials, pooling), null);
+ }
+ return ctx;
+ }
+
+ private InitialDirContext createInitialDirContextUsingGssapi(String principal, String credentials) throws NamingException {
+ Configuration.setConfiguration(new Krb5LoginConfiguration());
+ InitialDirContext initialDirContext;
+ try {
+ LoginContext lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, credentials));
+ lc.login();
+ initialDirContext = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<InitialDirContext>() {
+ @Override
+ public InitialDirContext run() throws NamingException {
+ Properties env = new Properties();
+ env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
+ env.put(Context.PROVIDER_URL, providerUrl);
+ env.put(Context.REFERRAL, referral);
+ return new InitialLdapContext(env, null);
+ }
+ });
+ } catch (LoginException | PrivilegedActionException e) {
+ NamingException namingException = new NamingException(e.getMessage());
+ namingException.initCause(e);
+ throw namingException;
+ }
+ return initialDirContext;
+ }
+
+ private Properties getEnvironment(@Nullable String principal, @Nullable String credentials, boolean pooling) {
+ Properties env = new Properties();
+ env.put(Context.SECURITY_AUTHENTICATION, authentication);
+ if (realm != null) {
+ env.put(SASL_REALM_PROPERTY, realm);
+ }
+ if (pooling) {
+ // Enable connection pooling
+ env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
+ }
+ env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
+ env.put(Context.PROVIDER_URL, providerUrl);
+ env.put(Context.REFERRAL, referral);
+ if (principal != null) {
+ env.put(Context.SECURITY_PRINCIPAL, principal);
+ }
+ // Note: debug is intentionally was placed here - in order to not expose password in log
+ LOG.debug("Initializing LDAP context {}", env);
+ if (credentials != null) {
+ env.put(Context.SECURITY_CREDENTIALS, credentials);
+ }
+ return env;
+ }
+
+ public boolean isSasl() {
+ return AUTH_METHOD_DIGEST_MD5.equals(authentication) ||
+ AUTH_METHOD_CRAM_MD5.equals(authentication) ||
+ AUTH_METHOD_GSSAPI.equals(authentication);
+ }
+
+ public boolean isGssapi() {
+ return AUTH_METHOD_GSSAPI.equals(authentication);
+ }
+
+ /**
+ * Tests connection.
+ *
+ * @throws LdapException if unable to open connection
+ */
+ public void testConnection() {
+ if (StringUtils.isBlank(username) && isSasl()) {
+ throw new IllegalArgumentException("When using SASL - property ldap.bindDn is required");
+ }
+ try {
+ createBindContext();
+ LOG.info("Test LDAP connection on {}: OK", providerUrl);
+ } catch (NamingException e) {
+ LOG.info("Test LDAP connection: FAIL");
+ throw new LdapException("Unable to open LDAP connection", e);
+ }
+ }
+
+ public String getProviderUrl() {
+ return providerUrl;
+ }
+
+ public String getReferral() {
+ return referral;
+ }
+
+ private static String getReferralsMode(Settings settings, String followReferralsSettingKey) {
+ if (settings.hasKey(followReferralsSettingKey)) {
+ return settings.getBoolean(followReferralsSettingKey) ? REFERRALS_FOLLOW_MODE : REFERRALS_IGNORE_MODE;
+ }
+ // By default follow referrals
+ return REFERRALS_FOLLOW_MODE;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{" +
+ "url=" + providerUrl +
+ ", authentication=" + authentication +
+ ", factory=" + factory +
+ ", bindDn=" + username +
+ ", realm=" + realm +
+ ", referral=" + referral +
+ "}";
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapException.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapException.java
new file mode 100644
index 00000000000..e582a7dc682
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapException.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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;
+
+public class LdapException extends RuntimeException {
+
+ public LdapException(String message) {
+ super(message);
+ }
+
+ public LdapException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupMapping.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupMapping.java
new file mode 100644
index 00000000000..a464345ebf4
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupMapping.java
@@ -0,0 +1,152 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.Arrays;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.SearchResult;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public class LdapGroupMapping {
+
+ private static final Logger LOG = Loggers.get(LdapGroupMapping.class);
+
+ private static final String DEFAULT_OBJECT_CLASS = "groupOfUniqueNames";
+ private static final String DEFAULT_ID_ATTRIBUTE = "cn";
+ private static final String DEFAULT_MEMBER_ATTRIBUTE = "uniqueMember";
+ private static final String DEFAULT_REQUEST = "(&(objectClass=groupOfUniqueNames)(uniqueMember={dn}))";
+
+ private final String baseDn;
+ private final String idAttribute;
+ private final String request;
+ private final String[] requiredUserAttributes;
+
+ /**
+ * Constructs mapping from Sonar settings.
+ */
+ public LdapGroupMapping(Settings settings, String settingsPrefix) {
+ this.baseDn = settings.getString(settingsPrefix + ".group.baseDn");
+ this.idAttribute = StringUtils.defaultString(settings.getString(settingsPrefix + ".group.idAttribute"), DEFAULT_ID_ATTRIBUTE);
+
+ String objectClass = settings.getString(settingsPrefix + ".group.objectClass");
+ String memberAttribute = settings.getString(settingsPrefix + ".group.memberAttribute");
+
+ String req;
+ if (StringUtils.isNotBlank(objectClass) || StringUtils.isNotBlank(memberAttribute)) {
+ // For backward compatibility with plugin versions 1.1 and 1.1.1
+ objectClass = StringUtils.defaultString(objectClass, DEFAULT_OBJECT_CLASS);
+ memberAttribute = StringUtils.defaultString(memberAttribute, DEFAULT_MEMBER_ATTRIBUTE);
+ req = "(&(objectClass=" + objectClass + ")(" + memberAttribute + "=" + "{dn}))";
+ LOG.warn("Properties '" + settingsPrefix + ".group.objectClass' and '" + settingsPrefix + ".group.memberAttribute' are deprecated" +
+ " and should be replaced by single property '" + settingsPrefix + ".group.request' with value: " + req);
+ } else {
+ req = StringUtils.defaultString(settings.getString(settingsPrefix + ".group.request"), DEFAULT_REQUEST);
+ }
+ this.requiredUserAttributes = StringUtils.substringsBetween(req, "{", "}");
+ for (int i = 0; i < requiredUserAttributes.length; i++) {
+ req = StringUtils.replace(req, "{" + requiredUserAttributes[i] + "}", "{" + i + "}");
+ }
+ this.request = req;
+ }
+
+ /**
+ * Search for this mapping.
+ */
+ public LdapSearch createSearch(LdapContextFactory contextFactory, SearchResult user) {
+ String[] attrs = getRequiredUserAttributes();
+ String[] parameters = new String[attrs.length];
+ for (int i = 0; i < parameters.length; i++) {
+ String attr = attrs[i];
+ if ("dn".equals(attr)) {
+ parameters[i] = user.getNameInNamespace();
+ } else {
+ parameters[i] = getAttributeValue(user, attr);
+ }
+ }
+ return new LdapSearch(contextFactory)
+ .setBaseDn(getBaseDn())
+ .setRequest(getRequest())
+ .setParameters(parameters)
+ .returns(getIdAttribute());
+ }
+
+ private static String getAttributeValue(SearchResult user, String attributeId) {
+ Attribute attribute = user.getAttributes().get(attributeId);
+ if (attribute == null) {
+ return null;
+ }
+ try {
+ return (String) attribute.get();
+ } catch (NamingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Base DN. For example "ou=groups,o=mycompany".
+ */
+ public String getBaseDn() {
+ return baseDn;
+ }
+
+ /**
+ * Group ID Attribute. For example "cn".
+ */
+ public String getIdAttribute() {
+ return idAttribute;
+ }
+
+ /**
+ * Request. For example:
+ * <pre>
+ * (&(objectClass=groupOfUniqueNames)(uniqueMember={0}))
+ * (&(objectClass=posixGroup)(memberUid={0}))
+ * (&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={0})(memberUid={1})))
+ * </pre>
+ */
+ public String getRequest() {
+ return request;
+ }
+
+ /**
+ * Attributes of user required for search of groups.
+ */
+ public String[] getRequiredUserAttributes() {
+ return requiredUserAttributes;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{" +
+ "baseDn=" + getBaseDn() +
+ ", idAttribute=" + getIdAttribute() +
+ ", requiredUserAttributes=" + Arrays.toString(getRequiredUserAttributes()) +
+ ", request=" + getRequest() +
+ "}";
+ }
+
+}
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
new file mode 100644
index 00000000000..9e279aef84a
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java
@@ -0,0 +1,144 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.security.ExternalGroupsProvider;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public class LdapGroupsProvider extends ExternalGroupsProvider {
+
+ private static final Logger LOG = Loggers.get(LdapGroupsProvider.class);
+
+ private final Map<String, LdapContextFactory> contextFactories;
+ private final Map<String, LdapUserMapping> userMappings;
+ private final Map<String, LdapGroupMapping> groupMappings;
+
+ 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;
+ }
+ }
+ 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/LdapModule.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapModule.java
new file mode 100644
index 00000000000..d4c6a4c7396
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapModule.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.core.platform.Module;
+
+public class LdapModule extends Module {
+
+ @Override
+ protected void configureModule() {
+ add(
+ LdapRealm.class,
+ LdapSettingsManager.class,
+ LdapAutodiscovery.class);
+ }
+
+}
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
new file mode 100644
index 00000000000..03918594e43
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 org.sonar.api.security.Authenticator;
+import org.sonar.api.security.ExternalGroupsProvider;
+import org.sonar.api.security.ExternalUsersProvider;
+import org.sonar.api.security.SecurityRealm;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public class LdapRealm extends SecurityRealm {
+
+ private LdapUsersProvider usersProvider;
+ private LdapGroupsProvider groupsProvider;
+ private LdapAuthenticator authenticator;
+ private final LdapSettingsManager settingsManager;
+
+ public LdapRealm(LdapSettingsManager settingsManager) {
+ 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);
+ Map<String, LdapGroupMapping> groupMappings = settingsManager.getGroupMappings();
+ if (!groupMappings.isEmpty()) {
+ groupsProvider = new LdapGroupsProvider(contextFactories, userMappings, groupMappings);
+ }
+ for (LdapContextFactory contextFactory : contextFactories.values()) {
+ contextFactory.testConnection();
+ }
+ }
+
+ @Override
+ public Authenticator doGetAuthenticator() {
+ return authenticator;
+ }
+
+ @Override
+ public ExternalUsersProvider getUsersProvider() {
+ return usersProvider;
+ }
+
+ @Override
+ public ExternalGroupsProvider getGroupsProvider() {
+ return groupsProvider;
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSearch.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSearch.java
new file mode 100644
index 00000000000..8606e647281
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSearch.java
@@ -0,0 +1,191 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.Arrays;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.PartialResultException;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+/**
+ * Fluent API for building LDAP queries.
+ *
+ * @author Evgeny Mandrikov
+ */
+public class LdapSearch {
+
+ private static final Logger LOG = Loggers.get(LdapSearch.class);
+
+ private final LdapContextFactory contextFactory;
+
+ private String baseDn;
+ private int scope = SearchControls.SUBTREE_SCOPE;
+ private String request;
+ private String[] parameters;
+ private String[] returningAttributes;
+
+ public LdapSearch(LdapContextFactory contextFactory) {
+ this.contextFactory = contextFactory;
+ }
+
+ /**
+ * Sets BaseDN.
+ */
+ public LdapSearch setBaseDn(String baseDn) {
+ this.baseDn = baseDn;
+ return this;
+ }
+
+ public String getBaseDn() {
+ return baseDn;
+ }
+
+ /**
+ * Sets the search scope.
+ *
+ * @see SearchControls#ONELEVEL_SCOPE
+ * @see SearchControls#SUBTREE_SCOPE
+ * @see SearchControls#OBJECT_SCOPE
+ */
+ public LdapSearch setScope(int scope) {
+ this.scope = scope;
+ return this;
+ }
+
+ public int getScope() {
+ return scope;
+ }
+
+ /**
+ * Sets request.
+ */
+ public LdapSearch setRequest(String request) {
+ this.request = request;
+ return this;
+ }
+
+ public String getRequest() {
+ return request;
+ }
+
+ /**
+ * Sets search parameters.
+ */
+ public LdapSearch setParameters(String... parameters) {
+ this.parameters = parameters;
+ return this;
+ }
+
+ public String[] getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Sets attributes, which should be returned by search.
+ */
+ public LdapSearch returns(String... attributes) {
+ this.returningAttributes = attributes;
+ return this;
+ }
+
+ public String[] getReturningAttributes() {
+ return returningAttributes;
+ }
+
+ /**
+ * @throws NamingException if unable to perform search
+ */
+ public NamingEnumeration<SearchResult> find() throws NamingException {
+ LOG.debug("Search: {}", this);
+ NamingEnumeration<SearchResult> result;
+ InitialDirContext context = null;
+ boolean threw = false;
+ try {
+ context = contextFactory.createBindContext();
+ SearchControls controls = new SearchControls();
+ controls.setSearchScope(scope);
+ controls.setReturningAttributes(returningAttributes);
+ result = context.search(baseDn, request, parameters, controls);
+ threw = true;
+ } finally {
+ ContextHelper.close(context, threw);
+ }
+ return result;
+ }
+
+ /**
+ * @return result, or null if not found
+ * @throws NamingException if unable to perform search, or non unique result
+ */
+ public SearchResult findUnique() throws NamingException {
+ NamingEnumeration<SearchResult> result = find();
+ if (hasMore(result)) {
+ SearchResult obj = result.next();
+ if (!hasMore(result)) {
+ return obj;
+ }
+ throw new NamingException("Non unique result for " + toString());
+ }
+ return null;
+ }
+
+ private static boolean hasMore(NamingEnumeration<SearchResult> result) throws NamingException {
+ try {
+ return result.hasMore();
+ } catch (PartialResultException e) {
+ LOG.debug("More result might be forthcoming if the referral is followed", e);
+ // See LDAP-62 and http://docs.oracle.com/javase/jndi/tutorial/ldap/referral/jndi.html :
+ // When the LDAP service provider receives a referral despite your having set Context.REFERRAL to "ignore", it will throw a
+ // PartialResultException(in the API reference documentation) to indicate that more results might be forthcoming if the referral is
+ // followed. In this case, the server does not support the Manage Referral control and is supporting referral updates in some other
+ // way.
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{" +
+ "baseDn=" + baseDn +
+ ", scope=" + scopeToString() +
+ ", request=" + request +
+ ", parameters=" + Arrays.toString(parameters) +
+ ", attributes=" + Arrays.toString(returningAttributes) +
+ "}";
+ }
+
+ private String scopeToString() {
+ switch (scope) {
+ case SearchControls.ONELEVEL_SCOPE:
+ return "onelevel";
+ case SearchControls.OBJECT_SCOPE:
+ return "object";
+ case SearchControls.SUBTREE_SCOPE:
+ default:
+ return "subtree";
+ }
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSettingsManager.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSettingsManager.java
new file mode 100644
index 00000000000..1bb460c8b42
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSettingsManager.java
@@ -0,0 +1,195 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.config.Settings;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static org.sonar.auth.ldap.LdapAutodiscovery.LdapSrvRecord;
+
+/**
+ * The LdapSettingsManager will parse the settings.
+ * This class is also responsible to cope with multiple ldap servers.
+ */
+@ServerSide
+public class LdapSettingsManager {
+
+ private static final Logger LOG = Loggers.get(LdapSettingsManager.class);
+
+ private static final String LDAP_SERVERS_PROPERTY = "ldap.servers";
+ private static final String LDAP_PROPERTY_PREFIX = "ldap";
+ private static final String DEFAULT_LDAP_SERVER_KEY = "<default>";
+ private final Settings settings;
+ private final LdapAutodiscovery ldapAutodiscovery;
+ private Map<String, LdapUserMapping> userMappings = null;
+ private Map<String, LdapGroupMapping> groupMappings = null;
+ private Map<String, LdapContextFactory> contextFactories;
+
+ /**
+ * Create an instance of the settings manager.
+ *
+ * @param settings The settings to use.
+ */
+ public LdapSettingsManager(Settings settings, LdapAutodiscovery ldapAutodiscovery) {
+ this.settings = settings;
+ this.ldapAutodiscovery = ldapAutodiscovery;
+ }
+
+ /**
+ * Get all the @link{LdapUserMapping}s available in the settings.
+ *
+ * @return A @link{Map} with all the @link{LdapUserMapping} objects.
+ * The key is the server key used in the settings (ldap for old single server notation).
+ */
+ public Map<String, LdapUserMapping> getUserMappings() {
+ if (userMappings == null) {
+ // Use linked hash map to preserve order
+ userMappings = new LinkedHashMap<>();
+ String[] serverKeys = settings.getStringArray(LDAP_SERVERS_PROPERTY);
+ if (serverKeys.length > 0) {
+ for (String serverKey : serverKeys) {
+ LdapUserMapping userMapping = new LdapUserMapping(settings, LDAP_PROPERTY_PREFIX + "." + serverKey);
+ if (StringUtils.isNotBlank(userMapping.getBaseDn())) {
+ LOG.info("User mapping for server {}: {}", serverKey, userMapping);
+ userMappings.put(serverKey, userMapping);
+ } else {
+ LOG.info("Users will not be synchronized for server {}, because property 'ldap.{}.user.baseDn' is empty.", serverKey, serverKey);
+ }
+ }
+ } else {
+ // Backward compatibility with single server configuration
+ LdapUserMapping userMapping = new LdapUserMapping(settings, LDAP_PROPERTY_PREFIX);
+ if (StringUtils.isNotBlank(userMapping.getBaseDn())) {
+ LOG.info("User mapping: {}", userMapping);
+ userMappings.put(DEFAULT_LDAP_SERVER_KEY, userMapping);
+ } else {
+ LOG.info("Users will not be synchronized, because property 'ldap.user.baseDn' is empty.");
+ }
+ }
+ }
+ return userMappings;
+ }
+
+ /**
+ * Get all the @link{LdapGroupMapping}s available in the settings.
+ *
+ * @return A @link{Map} with all the @link{LdapGroupMapping} objects.
+ * The key is the server key used in the settings (ldap for old single server notation).
+ */
+ public Map<String, LdapGroupMapping> getGroupMappings() {
+ if (groupMappings == null) {
+ // Use linked hash map to preserve order
+ groupMappings = new LinkedHashMap<>();
+ String[] serverKeys = settings.getStringArray(LDAP_SERVERS_PROPERTY);
+ if (serverKeys.length > 0) {
+ for (String serverKey : serverKeys) {
+ LdapGroupMapping groupMapping = new LdapGroupMapping(settings, LDAP_PROPERTY_PREFIX + "." + serverKey);
+ if (StringUtils.isNotBlank(groupMapping.getBaseDn())) {
+ LOG.info("Group mapping for server {}: {}", serverKey, groupMapping);
+ groupMappings.put(serverKey, groupMapping);
+ } else {
+ LOG.info("Groups will not be synchronized for server {}, because property 'ldap.{}.group.baseDn' is empty.", serverKey, serverKey);
+ }
+ }
+ } else {
+ // Backward compatibility with single server configuration
+ LdapGroupMapping groupMapping = new LdapGroupMapping(settings, LDAP_PROPERTY_PREFIX);
+ if (StringUtils.isNotBlank(groupMapping.getBaseDn())) {
+ LOG.info("Group mapping: {}", groupMapping);
+ groupMappings.put(DEFAULT_LDAP_SERVER_KEY, groupMapping);
+ } else {
+ LOG.info("Groups will not be synchronized, because property 'ldap.group.baseDn' is empty.");
+ }
+ }
+ }
+ return groupMappings;
+ }
+
+ /**
+ * Get all the @link{LdapContextFactory}s available in the settings.
+ *
+ * @return A @link{Map} with all the @link{LdapContextFactory} objects.
+ * The key is the server key used in the settings (ldap for old single server notation).
+ */
+ public Map<String, LdapContextFactory> getContextFactories() {
+ if (contextFactories == null) {
+ // Use linked hash map to preserve order
+ contextFactories = new LinkedHashMap<>();
+ String[] serverKeys = settings.getStringArray(LDAP_SERVERS_PROPERTY);
+ if (serverKeys.length > 0) {
+ initMultiLdapConfiguration(serverKeys);
+ } else {
+ initSimpleLdapConfiguration();
+ }
+ }
+ return contextFactories;
+ }
+
+ private void initSimpleLdapConfiguration() {
+ String realm = settings.getString(LDAP_PROPERTY_PREFIX + ".realm");
+ String ldapUrlKey = LDAP_PROPERTY_PREFIX + ".url";
+ String ldapUrl = settings.getString(ldapUrlKey);
+ if (ldapUrl == null && realm != null) {
+ LOG.warn("Auto-discovery feature is deprecated, please use '{}' to specify LDAP url", ldapUrlKey);
+ List<LdapSrvRecord> ldapServers = ldapAutodiscovery.getLdapServers(realm);
+ if (ldapServers.isEmpty()) {
+ throw new LdapException(String.format("The property '%s' is empty and SonarQube is not able to auto-discover any LDAP server.", ldapUrlKey));
+ }
+ int index = 1;
+ for (LdapSrvRecord ldapSrvRecord : ldapServers) {
+ if (StringUtils.isNotBlank(ldapSrvRecord.getServerUrl())) {
+ LOG.info("Detected server: {}", ldapSrvRecord.getServerUrl());
+ LdapContextFactory contextFactory = new LdapContextFactory(settings, LDAP_PROPERTY_PREFIX, ldapSrvRecord.getServerUrl());
+ contextFactories.put(DEFAULT_LDAP_SERVER_KEY + index, contextFactory);
+ index++;
+ }
+ }
+ } else {
+ if (StringUtils.isBlank(ldapUrl)) {
+ throw new LdapException(String.format("The property '%s' is empty and no realm configured to try auto-discovery.", ldapUrlKey));
+ }
+ LdapContextFactory contextFactory = new LdapContextFactory(settings, LDAP_PROPERTY_PREFIX, ldapUrl);
+ contextFactories.put(DEFAULT_LDAP_SERVER_KEY, contextFactory);
+ }
+ }
+
+ private void initMultiLdapConfiguration(String[] serverKeys) {
+ if (settings.hasKey("ldap.url") || settings.hasKey("ldap.realm")) {
+ throw new LdapException("When defining multiple LDAP servers with the property '" + LDAP_SERVERS_PROPERTY + "', "
+ + "all LDAP properties must be linked to one of those servers. Please remove properties like 'ldap.url', 'ldap.realm', ...");
+ }
+ for (String serverKey : serverKeys) {
+ String prefix = LDAP_PROPERTY_PREFIX + "." + serverKey;
+ String ldapUrlKey = prefix + ".url";
+ String ldapUrl = settings.getString(ldapUrlKey);
+ if (StringUtils.isBlank(ldapUrl)) {
+ throw new LdapException(String.format("The property '%s' property is empty while it is mandatory.", ldapUrlKey));
+ }
+ LdapContextFactory contextFactory = new LdapContextFactory(settings, prefix, ldapUrl);
+ contextFactories.put(serverKey, contextFactory);
+ }
+ }
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserMapping.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserMapping.java
new file mode 100644
index 00000000000..0faf4faf794
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserMapping.java
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.apache.commons.lang.StringUtils;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+/**
+ * @author Evgeny Mandrikov
+ */
+public class LdapUserMapping {
+
+ private static final Logger LOG = Loggers.get(LdapUserMapping.class);
+
+ private static final String DEFAULT_OBJECT_CLASS = "inetOrgPerson";
+ private static final String DEFAULT_LOGIN_ATTRIBUTE = "uid";
+ private static final String DEFAULT_NAME_ATTRIBUTE = "cn";
+ private static final String DEFAULT_EMAIL_ATTRIBUTE = "mail";
+ private static final String DEFAULT_REQUEST = "(&(objectClass=inetOrgPerson)(uid={login}))";
+
+ private final String baseDn;
+ private final String request;
+ private final String realNameAttribute;
+ private final String emailAttribute;
+
+ /**
+ * Constructs mapping from Sonar settings.
+ */
+ public LdapUserMapping(Settings settings, String settingsPrefix) {
+ String usesrBaseDnSettingKey = settingsPrefix + ".user.baseDn";
+ String usersBaseDn = settings.getString(usesrBaseDnSettingKey);
+ if (usersBaseDn == null) {
+ String realm = settings.getString(settingsPrefix + ".realm");
+ if (realm != null) {
+ LOG.warn("Auto-discovery feature is deprecated, please use '{}' to specify user search dn", usesrBaseDnSettingKey);
+ usersBaseDn = LdapAutodiscovery.getDnsDomainDn(realm);
+ }
+ }
+
+ String objectClass = settings.getString(settingsPrefix + ".user.objectClass");
+ String loginAttribute = settings.getString(settingsPrefix + ".user.loginAttribute");
+
+ this.baseDn = usersBaseDn;
+ this.realNameAttribute = StringUtils.defaultString(settings.getString(settingsPrefix + ".user.realNameAttribute"), DEFAULT_NAME_ATTRIBUTE);
+ this.emailAttribute = StringUtils.defaultString(settings.getString(settingsPrefix + ".user.emailAttribute"), DEFAULT_EMAIL_ATTRIBUTE);
+
+ String req;
+ if (StringUtils.isNotBlank(objectClass) || StringUtils.isNotBlank(loginAttribute)) {
+ objectClass = StringUtils.defaultString(objectClass, DEFAULT_OBJECT_CLASS);
+ loginAttribute = StringUtils.defaultString(loginAttribute, DEFAULT_LOGIN_ATTRIBUTE);
+ req = "(&(objectClass=" + objectClass + ")(" + loginAttribute + "={login}))";
+ // For backward compatibility with plugin versions lower than 1.2
+ Loggers.get(LdapGroupMapping.class)
+ .warn("Properties '{}.user.objectClass' and '{}.user.loginAttribute' are deprecated and should be " +
+ "replaced by single property '{}.user.request' with value: {}",
+ settingsPrefix, settingsPrefix, settingsPrefix, req);
+ } else {
+ req = StringUtils.defaultString(settings.getString(settingsPrefix + ".user.request"), DEFAULT_REQUEST);
+ }
+ req = StringUtils.replace(req, "{login}", "{0}");
+ this.request = req;
+ }
+
+ /**
+ * Search for this mapping.
+ */
+ public LdapSearch createSearch(LdapContextFactory contextFactory, String username) {
+ return new LdapSearch(contextFactory)
+ .setBaseDn(getBaseDn())
+ .setRequest(getRequest())
+ .setParameters(username);
+ }
+
+ /**
+ * Base DN. For example "ou=users,o=mycompany" or "cn=users" (Active Directory Server).
+ */
+ public String getBaseDn() {
+ return baseDn;
+ }
+
+ /**
+ * Request. For example:
+ * <pre>
+ * (&(objectClass=inetOrgPerson)(uid={0}))
+ * (&(objectClass=user)(sAMAccountName={0}))
+ * </pre>
+ */
+ public String getRequest() {
+ return request;
+ }
+
+ /**
+ * Real Name Attribute. For example "cn".
+ */
+ public String getRealNameAttribute() {
+ return realNameAttribute;
+ }
+
+ /**
+ * EMail Attribute. For example "mail".
+ */
+ public String getEmailAttribute() {
+ return emailAttribute;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{" +
+ "baseDn=" + getBaseDn() +
+ ", request=" + getRequest() +
+ ", realNameAttribute=" + getRealNameAttribute() +
+ ", emailAttribute=" + getEmailAttribute() +
+ "}";
+ }
+
+}
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
new file mode 100644
index 00000000000..2a81f78309b
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.security.ExternalUsersProvider;
+import org.sonar.api.security.UserDetails;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+
+/**
+ * @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;
+
+ public LdapUsersProvider(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 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);
+ }
+ 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;
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/package-info.java b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/package-info.java
new file mode 100644
index 00000000000..6d19b915ec5
--- /dev/null
+++ b/server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.auth.ldap;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/CallbackHandlerImplTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/CallbackHandlerImplTest.java
new file mode 100644
index 00000000000..c0acc7801c0
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/CallbackHandlerImplTest.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class CallbackHandlerImplTest {
+
+ @Test
+ public void test() throws Exception {
+ NameCallback nameCallback = new NameCallback("username");
+ PasswordCallback passwordCallback = new PasswordCallback("password", false);
+ new CallbackHandlerImpl("tester", "secret").handle(new Callback[] {nameCallback, passwordCallback});
+
+ assertThat(nameCallback.getName()).isEqualTo("tester");
+ assertThat(passwordCallback.getPassword()).isEqualTo("secret".toCharArray());
+ }
+
+ @Test(expected = UnsupportedCallbackException.class)
+ public void unsupportedCallback() throws Exception {
+ new CallbackHandlerImpl("tester", "secret").handle(new Callback[] {mock(Callback.class)});
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/ContextHelperTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/ContextHelperTest.java
new file mode 100644
index 00000000000..73029df57cd
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/ContextHelperTest.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 javax.naming.Context;
+import javax.naming.NamingException;
+import org.junit.Test;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+public class ContextHelperTest {
+
+ @Test
+ public void shouldSwallow() throws Exception {
+ Context context = mock(Context.class);
+ doThrow(new NamingException()).when(context).close();
+ ContextHelper.close(context, true);
+ ContextHelper.closeQuietly(context);
+ }
+
+ @Test(expected = NamingException.class)
+ public void shouldNotSwallow() throws Exception {
+ Context context = mock(Context.class);
+ doThrow(new NamingException()).when(context).close();
+ ContextHelper.close(context, false);
+ }
+
+ @Test
+ public void normal() throws NamingException {
+ ContextHelper.close(null, true);
+ ContextHelper.closeQuietly(null);
+ ContextHelper.close(mock(Context.class), true);
+ }
+
+}
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
new file mode 100644
index 00000000000..c49205a83b5
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.io.File;
+import javax.servlet.http.HttpServletRequest;
+import org.junit.Assert;
+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;
+
+public class KerberosTest {
+
+ static {
+ System.setProperty("java.security.krb5.conf", new File("target/krb5.conf").getAbsolutePath());
+ }
+
+ @ClassRule
+ public static LdapServer server = new LdapServer("/krb.ldif");
+
+ @Test
+ public void test() {
+ MapSettings settings = configure();
+ LdapRealm ldapRealm = new LdapRealm(new LdapSettingsManager(settings, new LdapAutodiscovery()));
+
+ ldapRealm.init();
+
+ assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new Authenticator.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();
+ // Using default realm from krb5.conf:
+ assertThat(ldapRealm.doGetAuthenticator().doAuthenticate(new Authenticator.Context("Godin", "user_password", Mockito.mock(HttpServletRequest.class)))).isTrue();
+
+ assertThat(ldapRealm.getGroupsProvider().doGetGroups(new ExternalGroupsProvider.Context("godin", Mockito.mock(HttpServletRequest.class)))).containsOnly("sonar-users");
+ }
+
+ @Test
+ public void wrong_bind_password() {
+ MapSettings settings = configure()
+ .setProperty("ldap.bindPassword", "wrong_bind_password");
+ LdapRealm ldapRealm = new LdapRealm(new LdapSettingsManager(settings, new LdapAutodiscovery()));
+ try {
+ ldapRealm.init();
+ Assert.fail();
+ } catch (LdapException e) {
+ assertThat(e.getMessage()).isEqualTo("Unable to open LDAP connection");
+ }
+ }
+
+ private static MapSettings configure() {
+ return new MapSettings()
+ .setProperty("ldap.url", server.getUrl())
+ .setProperty("ldap.authentication", LdapContextFactory.AUTH_METHOD_GSSAPI)
+ .setProperty("ldap.bindDn", "SonarQube@EXAMPLE.ORG")
+ .setProperty("ldap.bindPassword", "bind_password")
+ .setProperty("ldap.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.group.baseDn", "ou=groups,dc=example,dc=org")
+ .setProperty("ldap.group.request", "(&(objectClass=groupOfUniqueNames)(uniqueMember={dn}))");
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java
new file mode 100644
index 00000000000..da91691ee46
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java
@@ -0,0 +1,129 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.auth.ldap;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.auth.ldap.server.LdapServer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LdapAuthenticatorTest {
+
+ /**
+ * A reference to the original ldif file
+ */
+ public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif";
+ /**
+ * A reference to an aditional ldif file.
+ */
+ public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif";
+ @ClassRule
+ public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF);
+ @ClassRule
+ public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com");
+
+ @Test
+ public void testNoConnection() {
+ exampleServer.disableAnonymousAccess();
+ try {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_SIMPLE),
+ new LdapAutodiscovery());
+ LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings());
+ authenticator.authenticate("godin", "secret1");
+ } finally {
+ exampleServer.enableAnonymousAccess();
+ }
+ }
+
+ @Test
+ public void testSimple() {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_SIMPLE),
+ new LdapAutodiscovery());
+ LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings());
+
+ assertThat(authenticator.authenticate("godin", "secret1")).isTrue();
+ assertThat(authenticator.authenticate("godin", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("tester", "secret2")).isTrue();
+ assertThat(authenticator.authenticate("tester", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("notfound", "wrong")).isFalse();
+ // SONARPLUGINS-2493
+ assertThat(authenticator.authenticate("godin", "")).isFalse();
+ assertThat(authenticator.authenticate("godin", null)).isFalse();
+ }
+
+ @Test
+ public void testSimpleMultiLdap() {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ LdapSettingsFactory.generateAuthenticationSettings(exampleServer, infosupportServer, LdapContextFactory.AUTH_METHOD_SIMPLE), new LdapAutodiscovery());
+ LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings());
+
+ assertThat(authenticator.authenticate("godin", "secret1")).isTrue();
+ assertThat(authenticator.authenticate("godin", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("tester", "secret2")).isTrue();
+ assertThat(authenticator.authenticate("tester", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("notfound", "wrong")).isFalse();
+ // SONARPLUGINS-2493
+ assertThat(authenticator.authenticate("godin", "")).isFalse();
+ assertThat(authenticator.authenticate("godin", null)).isFalse();
+
+ // SONARPLUGINS-2793
+ assertThat(authenticator.authenticate("robby", "secret1")).isTrue();
+ assertThat(authenticator.authenticate("robby", "wrong")).isFalse();
+ }
+
+ @Test
+ public void testSasl() {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(LdapSettingsFactory.generateAuthenticationSettings(exampleServer, null, LdapContextFactory.AUTH_METHOD_CRAM_MD5),
+ new LdapAutodiscovery());
+ LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings());
+
+ assertThat(authenticator.authenticate("godin", "secret1")).isTrue();
+ assertThat(authenticator.authenticate("godin", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("tester", "secret2")).isTrue();
+ assertThat(authenticator.authenticate("tester", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("notfound", "wrong")).isFalse();
+ }
+
+ @Test
+ public void testSaslMultipleLdap() {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ LdapSettingsFactory.generateAuthenticationSettings(exampleServer, infosupportServer, LdapContextFactory.AUTH_METHOD_CRAM_MD5), new LdapAutodiscovery());
+ LdapAuthenticator authenticator = new LdapAuthenticator(settingsManager.getContextFactories(), settingsManager.getUserMappings());
+
+ assertThat(authenticator.authenticate("godin", "secret1")).isTrue();
+ assertThat(authenticator.authenticate("godin", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("tester", "secret2")).isTrue();
+ assertThat(authenticator.authenticate("tester", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("notfound", "wrong")).isFalse();
+
+ assertThat(authenticator.authenticate("robby", "secret1")).isTrue();
+ assertThat(authenticator.authenticate("robby", "wrong")).isFalse();
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java
new file mode 100644
index 00000000000..d002577a44c
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.auth.ldap;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.auth.ldap.server.ApacheDS;
+import org.sonar.auth.ldap.server.LdapServer;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class LdapAutoDiscoveryWarningLogTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @ClassRule
+ public static LdapServer server = new LdapServer("/users.example.org.ldif");
+
+ @Test
+ public void does_not_display_log_when_not_using_auto_discovery() {
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.url", server.getUrl());
+ LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings, new LdapAutodiscovery()));
+ assertThat(realm.getName()).isEqualTo("LDAP");
+
+ realm.init();
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty();
+ }
+
+ @Test
+ public void display_warning_log_when_using_auto_discovery_to_detect_server_url() {
+ LdapAutodiscovery ldapAutodiscovery = mock(LdapAutodiscovery.class);
+ when(ldapAutodiscovery.getLdapServers("example.org")).thenReturn(singletonList(new LdapAutodiscovery.LdapSrvRecord(server.getUrl(), 1, 1)));
+ // ldap.url setting is not set
+ LdapRealm realm = new LdapRealm(new LdapSettingsManager(new MapSettings().setProperty("ldap.realm", "example.org"),
+ ldapAutodiscovery));
+
+ realm.init();
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).contains("Auto-discovery feature is deprecated, please use 'ldap.url' to specify LDAP url");
+ }
+
+ @Test
+ public void display_warning_log_when_using_auto_discovery_to_detect_user_baseDn_on_single_server() {
+ // ldap.user.baseDn setting is not set
+ MapSettings settings = new MapSettings().setProperty("ldap.url", server.getUrl()).setProperty("ldap.realm", "example.org");
+ LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings, new LdapAutodiscovery()));
+
+ realm.init();
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly("Auto-discovery feature is deprecated, please use 'ldap.user.baseDn' to specify user search dn");
+ }
+
+ @Test
+ public void display_warning_log_when_using_auto_discovery_to_detect_user_baseDn_on_multiple_servers() throws Exception {
+ ApacheDS server2 = ApacheDS.start("example.org", "dc=example,dc=org", "target/ldap-work2/");
+ server2.importLdif(LdapAutoDiscoveryWarningLogTest.class.getResourceAsStream("/users.example.org.ldif"));
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.servers", "example,infosupport")
+ // ldap.XXX.user.baseDn settings are not set on both servers
+ .setProperty("ldap.example.url", server.getUrl())
+ .setProperty("ldap.example.realm", "example.org")
+ .setProperty("ldap.infosupport.url", server2.getUrl())
+ .setProperty("ldap.infosupport.realm", "infosupport.org");
+ LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings, new LdapAutodiscovery()));
+
+ realm.init();
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly(
+ "Auto-discovery feature is deprecated, please use 'ldap.example.user.baseDn' to specify user search dn",
+ "Auto-discovery feature is deprecated, please use 'ldap.infosupport.user.baseDn' to specify user search dn");
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutodiscoveryTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutodiscoveryTest.java
new file mode 100644
index 00000000000..989a3305fd7
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutodiscoveryTest.java
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.net.UnknownHostException;
+import java.util.Arrays;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.auth.ldap.LdapAutodiscovery.LdapSrvRecord;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class LdapAutodiscoveryTest {
+
+ @Test
+ public void testGetDnsDomain() {
+ assertThat(LdapAutodiscovery.getDnsDomainName("localhost")).isNull();
+ assertThat(LdapAutodiscovery.getDnsDomainName("godin.example.org")).isEqualTo("example.org");
+ assertThat(LdapAutodiscovery.getDnsDomainName("godin.usr.example.org")).isEqualTo("usr.example.org");
+ }
+
+ @Test
+ public void testGetDnsDomainWithoutParameter() {
+ try {
+ LdapAutodiscovery.getDnsDomainName();
+ } catch (UnknownHostException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ @Test
+ public void testGetDnsDomainDn() {
+ assertThat(LdapAutodiscovery.getDnsDomainDn("example.org")).isEqualTo("dc=example,dc=org");
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ assertThat(new LdapSrvRecord("http://foo:389", 1, 1)).isEqualTo(new LdapSrvRecord("http://foo:389", 2, 0));
+ assertThat(new LdapSrvRecord("http://foo:389", 1, 1)).isNotEqualTo(new LdapSrvRecord("http://foo:388", 1, 1));
+
+ assertThat(new LdapSrvRecord("http://foo:389", 1, 1).hashCode()).isEqualTo(new LdapSrvRecord("http://foo:389", 1, 1).hashCode());
+ }
+
+ @Test
+ public void testGetLdapServer() throws NamingException {
+ DirContext context = mock(DirContext.class);
+ Attributes attributes = mock(Attributes.class);
+ Attribute attribute = mock(Attribute.class);
+ NamingEnumeration namingEnumeration = mock(NamingEnumeration.class);
+
+ when(context.getAttributes(Mockito.anyString(), Mockito.<String[]>anyObject())).thenReturn(attributes);
+ when(attributes.get(Mockito.eq("srv"))).thenReturn(attribute);
+ when(attribute.getAll()).thenReturn(namingEnumeration);
+ when(namingEnumeration.hasMore()).thenReturn(true, true, true, true, true, false);
+ when(namingEnumeration.next())
+ .thenReturn("10 40 389 ldap5.example.org.")
+ .thenReturn("0 10 389 ldap3.example.org")
+ .thenReturn("0 60 389 ldap1.example.org")
+ .thenReturn("0 30 389 ldap2.example.org")
+ .thenReturn("10 60 389 ldap4.example.org");
+
+ assertThat(new LdapAutodiscovery().getLdapServers(context, "example.org.")).extracting("serverUrl")
+ .isEqualTo(
+ Arrays.asList("ldap://ldap1.example.org:389", "ldap://ldap2.example.org:389", "ldap://ldap3.example.org:389", "ldap://ldap4.example.org:389",
+ "ldap://ldap5.example.org:389"));
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupMappingTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupMappingTest.java
new file mode 100644
index 00000000000..5a9acab0469
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupMappingTest.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.auth.ldap;
+
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LdapGroupMappingTest {
+
+ @Test
+ public void defaults() {
+ LdapGroupMapping groupMapping = new LdapGroupMapping(new MapSettings(), "ldap");
+
+ assertThat(groupMapping.getBaseDn()).isNull();
+ assertThat(groupMapping.getIdAttribute()).isEqualTo("cn");
+ assertThat(groupMapping.getRequest()).isEqualTo("(&(objectClass=groupOfUniqueNames)(uniqueMember={0}))");
+ assertThat(groupMapping.getRequiredUserAttributes()).isEqualTo(new String[] {"dn"});
+
+ assertThat(groupMapping.toString()).isEqualTo("LdapGroupMapping{" +
+ "baseDn=null," +
+ " idAttribute=cn," +
+ " requiredUserAttributes=[dn]," +
+ " request=(&(objectClass=groupOfUniqueNames)(uniqueMember={0}))}");
+ }
+
+ @Test
+ public void backward_compatibility() {
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.group.objectClass", "group")
+ .setProperty("ldap.group.memberAttribute", "member");
+ LdapGroupMapping groupMapping = new LdapGroupMapping(settings, "ldap");
+
+ assertThat(groupMapping.getRequest()).isEqualTo("(&(objectClass=group)(member={0}))");
+ }
+
+ @Test
+ public void custom_request() {
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.group.request", "(&(|(objectClass=posixGroup)(objectClass=groupOfUniqueNames))(|(memberUid={uid})(uniqueMember={dn})))");
+ LdapGroupMapping groupMapping = new LdapGroupMapping(settings, "ldap");
+
+ assertThat(groupMapping.getRequest()).isEqualTo("(&(|(objectClass=posixGroup)(objectClass=groupOfUniqueNames))(|(memberUid={0})(uniqueMember={1})))");
+ assertThat(groupMapping.getRequiredUserAttributes()).isEqualTo(new String[] {"uid", "dn"});
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java
new file mode 100644
index 00000000000..a2aab4bc3c0
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.auth.ldap;
+
+import java.util.Collection;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.auth.ldap.server.LdapServer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LdapGroupsProviderTest {
+
+ /**
+ * A reference to the original ldif file
+ */
+ public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif";
+ /**
+ * A reference to an aditional ldif file.
+ */
+ public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif";
+
+ @ClassRule
+ public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF);
+ @ClassRule
+ public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com");
+
+ @Test
+ public void defaults() throws Exception {
+ MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, null);
+
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+ LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings());
+ Collection<String> groups;
+
+ groups = groupsProvider.getGroups("tester");
+ assertThat(groups).containsOnly("sonar-users");
+
+ groups = groupsProvider.getGroups("godin");
+ assertThat(groups).containsOnly("sonar-users", "sonar-developers");
+
+ groups = groupsProvider.getGroups("notfound");
+ assertThat(groups).isEmpty();
+ }
+
+ @Test
+ public void defaultsMultipleLdap() {
+ MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer);
+
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+ LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings());
+
+ Collection<String> groups;
+
+ groups = groupsProvider.getGroups("tester");
+ assertThat(groups).containsOnly("sonar-users");
+
+ groups = groupsProvider.getGroups("godin");
+ assertThat(groups).containsOnly("sonar-users", "sonar-developers");
+
+ groups = groupsProvider.getGroups("notfound");
+ assertThat(groups).isEmpty();
+
+ groups = groupsProvider.getGroups("testerInfo");
+ assertThat(groups).containsOnly("sonar-users");
+
+ groups = groupsProvider.getGroups("robby");
+ assertThat(groups).containsOnly("sonar-users", "sonar-developers");
+ }
+
+ @Test
+ public void posix() {
+ MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, null);
+ settings.setProperty("ldap.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))");
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+ LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings());
+
+ Collection<String> groups;
+
+ groups = groupsProvider.getGroups("godin");
+ assertThat(groups).containsOnly("linux-users");
+ }
+
+ @Test
+ public void posixMultipleLdap() {
+ MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer);
+ settings.setProperty("ldap.example.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))");
+ settings.setProperty("ldap.infosupport.group.request", "(&(objectClass=posixGroup)(memberUid={uid}))");
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+ LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings());
+
+ Collection<String> groups;
+
+ groups = groupsProvider.getGroups("godin");
+ assertThat(groups).containsOnly("linux-users");
+
+ groups = groupsProvider.getGroups("robby");
+ assertThat(groups).containsOnly("linux-users");
+ }
+
+ @Test
+ public void mixed() {
+ MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer);
+ settings.setProperty("ldap.example.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))");
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+ LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings());
+
+ Collection<String> groups;
+
+ groups = groupsProvider.getGroups("godin");
+ assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users");
+ }
+
+ @Test
+ public void mixedMultipleLdap() {
+ MapSettings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer);
+ settings.setProperty("ldap.example.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))");
+ settings.setProperty("ldap.infosupport.group.request", "(&(|(objectClass=groupOfUniqueNames)(objectClass=posixGroup))(|(uniqueMember={dn})(memberUid={uid})))");
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+ LdapGroupsProvider groupsProvider = new LdapGroupsProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings(), settingsManager.getGroupMappings());
+
+ Collection<String> groups;
+
+ groups = groupsProvider.getGroups("godin");
+ assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users");
+
+ groups = groupsProvider.getGroups("robby");
+ assertThat(groups).containsOnly("sonar-users", "sonar-developers", "linux-users");
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapModuleTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapModuleTest.java
new file mode 100644
index 00000000000..c231f765af2
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapModuleTest.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.auth.ldap;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class LdapModuleTest {
+
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new LdapModule().configure(container);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 3);
+ }
+
+}
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
new file mode 100644
index 00000000000..232269a4083
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 javax.servlet.http.HttpServletRequest;
+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;
+import static org.junit.Assert.fail;
+
+public class LdapRealmTest {
+
+ @ClassRule
+ public static LdapServer server = new LdapServer("/users.example.org.ldif");
+
+ @Test
+ public void normal() {
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.url", server.getUrl());
+ LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings, 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.getGroupsProvider()).isNull();
+ }
+
+ @Test
+ public void noConnection() {
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.url", "ldap://no-such-host")
+ .setProperty("ldap.group.baseDn", "cn=groups,dc=example,dc=org");
+ LdapRealm realm = new LdapRealm(new LdapSettingsManager(settings, 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);
+
+ try {
+ realm.getUsersProvider().doGetUserDetails(new ExternalUsersProvider.Context("tester", Mockito.mock(HttpServletRequest.class)));
+ 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)));
+ fail("Since there is no connection, the doGetGroups method has to throw an exception.");
+ } catch (LdapException e) {
+ assertThat(e.getMessage()).contains("Unable to retrieve details for user tester");
+ }
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapReferralsTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapReferralsTest.java
new file mode 100644
index 00000000000..a635eea0a0f
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapReferralsTest.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.auth.ldap.server.LdapServer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LdapReferralsTest {
+
+ @ClassRule
+ public static LdapServer server = new LdapServer("/users.example.org.ldif");
+
+ Map<String, LdapContextFactory> underTest;
+
+ @Test
+ public void referral_is_set_to_follow_when_followReferrals_setting_is_set_to_true() {
+ underTest = createFactories("ldap.followReferrals", "true");
+
+ LdapContextFactory contextFactory = underTest.values().iterator().next();
+ assertThat(contextFactory.getReferral()).isEqualTo("follow");
+ }
+
+ @Test
+ public void referral_is_set_to_ignore_when_followReferrals_setting_is_set_to_false() {
+ underTest = createFactories("ldap.followReferrals", "false");
+
+ LdapContextFactory contextFactory = underTest.values().iterator().next();
+ assertThat(contextFactory.getReferral()).isEqualTo("ignore");
+ }
+
+ @Test
+ public void referral_is_set_to_follow_when_no_followReferrals_setting() {
+ underTest = createFactories(null, null);
+
+ LdapContextFactory contextFactory = underTest.values().iterator().next();
+ assertThat(contextFactory.getReferral()).isEqualTo("follow");
+ }
+
+ private static Map<String, LdapContextFactory> createFactories(@Nullable String propertyKey, @Nullable String propertyValue) {
+ Settings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(server, null);
+ if (propertyKey != null) {
+ settings.setProperty(propertyKey, propertyValue);
+ }
+ return new LdapSettingsManager(settings, new LdapAutodiscovery()).getContextFactories();
+ }
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSearchTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSearchTest.java
new file mode 100644
index 00000000000..d1b99d5169c
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSearchTest.java
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.Enumeration;
+import java.util.Map;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchControls;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.auth.ldap.server.LdapServer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LdapSearchTest {
+
+ @ClassRule
+ public static LdapServer server = new LdapServer("/users.example.org.ldif");
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private static Map<String, LdapContextFactory> contextFactories;
+
+ @BeforeClass
+ public static void init() {
+ contextFactories = new LdapSettingsManager(LdapSettingsFactory.generateSimpleAnonymousAccessSettings(server, null), new LdapAutodiscovery()).getContextFactories();
+ }
+
+ @Test
+ public void subtreeSearch() throws Exception {
+ LdapSearch search = new LdapSearch(contextFactories.values().iterator().next())
+ .setBaseDn("dc=example,dc=org")
+ .setRequest("(objectClass={0})")
+ .setParameters("inetOrgPerson")
+ .returns("objectClass");
+
+ assertThat(search.getBaseDn()).isEqualTo("dc=example,dc=org");
+ assertThat(search.getScope()).isEqualTo(SearchControls.SUBTREE_SCOPE);
+ assertThat(search.getRequest()).isEqualTo("(objectClass={0})");
+ assertThat(search.getParameters()).isEqualTo(new String[] {"inetOrgPerson"});
+ assertThat(search.getReturningAttributes()).isEqualTo(new String[] {"objectClass"});
+ assertThat(search.toString()).isEqualTo("LdapSearch{baseDn=dc=example,dc=org, scope=subtree, request=(objectClass={0}), parameters=[inetOrgPerson], attributes=[objectClass]}");
+ assertThat(enumerationToArrayList(search.find()).size()).isEqualTo(3);
+ thrown.expect(NamingException.class);
+ thrown.expectMessage("Non unique result for " + search.toString());
+ search.findUnique();
+ }
+
+ @Test
+ public void oneLevelSearch() throws Exception {
+ LdapSearch search = new LdapSearch(contextFactories.values().iterator().next())
+ .setBaseDn("dc=example,dc=org")
+ .setScope(SearchControls.ONELEVEL_SCOPE)
+ .setRequest("(objectClass={0})")
+ .setParameters("inetOrgPerson")
+ .returns("cn");
+
+ assertThat(search.getBaseDn()).isEqualTo("dc=example,dc=org");
+ assertThat(search.getScope()).isEqualTo(SearchControls.ONELEVEL_SCOPE);
+ assertThat(search.getRequest()).isEqualTo("(objectClass={0})");
+ assertThat(search.getParameters()).isEqualTo(new String[] {"inetOrgPerson"});
+ assertThat(search.getReturningAttributes()).isEqualTo(new String[] {"cn"});
+ assertThat(search.toString()).isEqualTo("LdapSearch{baseDn=dc=example,dc=org, scope=onelevel, request=(objectClass={0}), parameters=[inetOrgPerson], attributes=[cn]}");
+ assertThat(enumerationToArrayList(search.find()).size()).isEqualTo(0);
+ assertThat(search.findUnique()).isNull();
+ }
+
+ @Test
+ public void objectSearch() throws Exception {
+ LdapSearch search = new LdapSearch(contextFactories.values().iterator().next())
+ .setBaseDn("cn=bind,ou=users,dc=example,dc=org")
+ .setScope(SearchControls.OBJECT_SCOPE)
+ .setRequest("(objectClass={0})")
+ .setParameters("uidObject")
+ .returns("uid");
+
+ assertThat(search.getBaseDn()).isEqualTo("cn=bind,ou=users,dc=example,dc=org");
+ assertThat(search.getScope()).isEqualTo(SearchControls.OBJECT_SCOPE);
+ assertThat(search.getRequest()).isEqualTo("(objectClass={0})");
+ assertThat(search.getParameters()).isEqualTo(new String[] {"uidObject"});
+ assertThat(search.getReturningAttributes()).isEqualTo(new String[] {"uid"});
+ assertThat(search.toString()).isEqualTo(
+ "LdapSearch{baseDn=cn=bind,ou=users,dc=example,dc=org, scope=object, request=(objectClass={0}), parameters=[uidObject], attributes=[uid]}");
+ assertThat(enumerationToArrayList(search.find()).size()).isEqualTo(1);
+ assertThat(search.findUnique()).isNotNull();
+ }
+
+ private static <E> ArrayList<E> enumerationToArrayList(Enumeration<E> enumeration) {
+ ArrayList<E> result = new ArrayList<>();
+ while (enumeration.hasMoreElements()) {
+ result.add(enumeration.nextElement());
+ }
+ return result;
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsFactory.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsFactory.java
new file mode 100644
index 00000000000..d08b3d8f407
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsFactory.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 javax.annotation.Nullable;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.auth.ldap.server.LdapServer;
+
+/**
+ * Create Settings for most used test cases.
+ */
+public class LdapSettingsFactory {
+
+ /**
+ * Generate simple settings for 2 ldap servers that allows anonymous access.
+ *
+ * @return The specific settings.
+ */
+ public static MapSettings generateSimpleAnonymousAccessSettings(LdapServer exampleServer, @Nullable LdapServer infosupportServer) {
+ MapSettings settings = new MapSettings();
+
+ if (infosupportServer != null) {
+ settings.setProperty("ldap.servers", "example,infosupport");
+
+ settings.setProperty("ldap.example.url", exampleServer.getUrl())
+ .setProperty("ldap.example.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.example.group.baseDn", "ou=groups,dc=example,dc=org");
+ settings.setProperty("ldap.infosupport.url", infosupportServer.getUrl())
+ .setProperty("ldap.infosupport.user.baseDn", "ou=users,dc=infosupport,dc=com")
+ .setProperty("ldap.infosupport.group.baseDn", "ou=groups,dc=infosupport,dc=com");
+ } else {
+ settings.setProperty("ldap.url", exampleServer.getUrl())
+ .setProperty("ldap.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.group.baseDn", "ou=groups,dc=example,dc=org");
+ }
+ return settings;
+ }
+
+ /**
+ * Generate settings for 2 ldap servers.
+ *
+ * @param exampleServer The first ldap server.
+ * @param infosupportServer The second ldap server.
+ * @return The specific settings.
+ */
+ public static MapSettings generateAuthenticationSettings(LdapServer exampleServer, @Nullable LdapServer infosupportServer, String authMethod) {
+ MapSettings settings = new MapSettings();
+
+ if (infosupportServer != null) {
+ settings.setProperty("ldap.servers", "example,infosupport");
+
+ settings.setProperty("ldap.example.url", exampleServer.getUrl())
+ .setProperty("ldap.example.bindDn", LdapContextFactory.AUTH_METHOD_SIMPLE.equals(authMethod) ? "cn=bind,ou=users,dc=example,dc=org" : "bind")
+ .setProperty("ldap.example.bindPassword", "bindpassword")
+ .setProperty("ldap.example.authentication", authMethod)
+ .setProperty("ldap.example.realm", "example.org")
+ .setProperty("ldap.example.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.example.group.baseDn", "ou=groups,dc=example,dc=org");
+
+ settings.setProperty("ldap.infosupport.url", infosupportServer.getUrl())
+ .setProperty("ldap.infosupport.bindDn", LdapContextFactory.AUTH_METHOD_SIMPLE.equals(authMethod) ? "cn=bind,ou=users,dc=infosupport,dc=com" : "bind")
+ .setProperty("ldap.infosupport.bindPassword", "bindpassword")
+ .setProperty("ldap.infosupport.authentication", authMethod)
+ .setProperty("ldap.infosupport.realm", "infosupport.com")
+ .setProperty("ldap.infosupport.user.baseDn", "ou=users,dc=infosupport,dc=com")
+ .setProperty("ldap.infosupport.group.baseDn", "ou=groups,dc=infosupport,dc=com");
+ } else {
+ settings.setProperty("ldap.url", exampleServer.getUrl())
+ .setProperty("ldap.bindDn", LdapContextFactory.AUTH_METHOD_SIMPLE.equals(authMethod) ? "cn=bind,ou=users,dc=example,dc=org" : "bind")
+ .setProperty("ldap.bindPassword", "bindpassword")
+ .setProperty("ldap.authentication", authMethod)
+ .setProperty("ldap.realm", "example.org")
+ .setProperty("ldap.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.group.baseDn", "ou=groups,dc=example,dc=org");
+ }
+ return settings;
+ }
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsManagerTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsManagerTest.java
new file mode 100644
index 00000000000..88d16dedec8
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsManagerTest.java
@@ -0,0 +1,201 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.Arrays;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.Settings;
+import org.sonar.api.config.internal.MapSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.auth.ldap.LdapAutodiscovery.LdapSrvRecord;
+
+public class LdapSettingsManagerTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void shouldFailWhenNoLdapUrl() {
+ Settings settings = generateMultipleLdapSettingsWithUserAndGroupMapping();
+ settings.removeProperty("ldap.example.url");
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+
+ thrown.expect(LdapException.class);
+ thrown.expectMessage("The property 'ldap.example.url' property is empty while it is mandatory.");
+ settingsManager.getContextFactories();
+ }
+
+ @Test
+ public void shouldFailWhenMixingSingleAndMultipleConfiguration() {
+ Settings settings = generateMultipleLdapSettingsWithUserAndGroupMapping();
+ settings.setProperty("ldap.url", "ldap://foo");
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+
+ thrown.expect(LdapException.class);
+ thrown
+ .expectMessage(
+ "When defining multiple LDAP servers with the property 'ldap.servers', all LDAP properties must be linked to one of those servers. Please remove properties like 'ldap.url', 'ldap.realm', ...");
+ settingsManager.getContextFactories();
+ }
+
+ @Test
+ public void testContextFactoriesWithSingleLdap() throws Exception {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ generateSingleLdapSettingsWithUserAndGroupMapping(), new LdapAutodiscovery());
+ assertThat(settingsManager.getContextFactories().size()).isEqualTo(1);
+ }
+
+ /**
+ * Test there are 2 @link{org.sonar.plugins.ldap.LdapContextFactory}s found.
+ *
+ * @throws Exception
+ * This is not expected.
+ */
+ @Test
+ public void testContextFactoriesWithMultipleLdap() throws Exception {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ generateMultipleLdapSettingsWithUserAndGroupMapping(), new LdapAutodiscovery());
+ assertThat(settingsManager.getContextFactories().size()).isEqualTo(2);
+ // We do it twice to make sure the settings keep the same.
+ assertThat(settingsManager.getContextFactories().size()).isEqualTo(2);
+ }
+
+ @Test
+ public void testAutodiscover() throws Exception {
+ LdapAutodiscovery ldapAutodiscovery = mock(LdapAutodiscovery.class);
+ LdapSrvRecord ldap1 = new LdapSrvRecord("ldap://localhost:189", 1, 1);
+ LdapSrvRecord ldap2 = new LdapSrvRecord("ldap://localhost:1899", 1, 1);
+ when(ldapAutodiscovery.getLdapServers("example.org")).thenReturn(Arrays.asList(ldap1, ldap2));
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ generateAutodiscoverSettings(), ldapAutodiscovery);
+ assertThat(settingsManager.getContextFactories().size()).isEqualTo(2);
+ }
+
+ @Test
+ public void testAutodiscoverFailed() throws Exception {
+ LdapAutodiscovery ldapAutodiscovery = mock(LdapAutodiscovery.class);
+ when(ldapAutodiscovery.getLdapServers("example.org")).thenReturn(Collections.<LdapSrvRecord>emptyList());
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ generateAutodiscoverSettings(), ldapAutodiscovery);
+
+ thrown.expect(LdapException.class);
+ thrown.expectMessage("The property 'ldap.url' is empty and SonarQube is not able to auto-discover any LDAP server.");
+
+ settingsManager.getContextFactories();
+ }
+
+ /**
+ * Test there are 2 @link{org.sonar.plugins.ldap.LdapUserMapping}s found.
+ *
+ * @throws Exception
+ * This is not expected.
+ */
+ @Test
+ public void testUserMappings() throws Exception {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ generateMultipleLdapSettingsWithUserAndGroupMapping(), new LdapAutodiscovery());
+ assertThat(settingsManager.getUserMappings().size()).isEqualTo(2);
+ // We do it twice to make sure the settings keep the same.
+ assertThat(settingsManager.getUserMappings().size()).isEqualTo(2);
+ }
+
+ /**
+ * Test there are 2 @link{org.sonar.plugins.ldap.LdapGroupMapping}s found.
+ *
+ * @throws Exception
+ * This is not expected.
+ */
+ @Test
+ public void testGroupMappings() throws Exception {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ generateMultipleLdapSettingsWithUserAndGroupMapping(), new LdapAutodiscovery());
+ assertThat(settingsManager.getGroupMappings().size()).isEqualTo(2);
+ // We do it twice to make sure the settings keep the same.
+ assertThat(settingsManager.getGroupMappings().size()).isEqualTo(2);
+ }
+
+ /**
+ * Test what happens when no configuration is set.
+ * Normally there will be a contextFactory, but the autodiscovery doesn't work for the test server.
+ * @throws Exception
+ */
+ @Test
+ public void testEmptySettings() throws Exception {
+ LdapSettingsManager settingsManager = new LdapSettingsManager(
+ new MapSettings(), new LdapAutodiscovery());
+
+ thrown.expect(LdapException.class);
+ thrown.expectMessage("The property 'ldap.url' is empty and no realm configured to try auto-discovery.");
+ settingsManager.getContextFactories();
+ }
+
+ private MapSettings generateMultipleLdapSettingsWithUserAndGroupMapping() {
+ MapSettings settings = new MapSettings();
+
+ settings.setProperty("ldap.servers", "example,infosupport");
+
+ settings.setProperty("ldap.example.url", "/users.example.org.ldif")
+ .setProperty("ldap.example.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.example.group.baseDn", "ou=groups,dc=example,dc=org")
+ .setProperty("ldap.example.group.request",
+ "(&(objectClass=posixGroup)(memberUid={uid}))");
+
+ settings.setProperty("ldap.infosupport.url", "/users.infosupport.com.ldif")
+ .setProperty("ldap.infosupport.user.baseDn",
+ "ou=users,dc=infosupport,dc=com")
+ .setProperty("ldap.infosupport.group.baseDn",
+ "ou=groups,dc=infosupport,dc=com")
+ .setProperty("ldap.infosupport.group.request",
+ "(&(objectClass=posixGroup)(memberUid={uid}))");
+
+ return settings;
+ }
+
+ private MapSettings generateSingleLdapSettingsWithUserAndGroupMapping() {
+ MapSettings settings = new MapSettings();
+
+ settings.setProperty("ldap.url", "/users.example.org.ldif")
+ .setProperty("ldap.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.group.baseDn", "ou=groups,dc=example,dc=org")
+ .setProperty("ldap.group.request",
+ "(&(objectClass=posixGroup)(memberUid={uid}))");
+
+ return settings;
+ }
+
+ private MapSettings generateAutodiscoverSettings() {
+ MapSettings settings = new MapSettings();
+
+ settings.setProperty("ldap.realm", "example.org")
+ .setProperty("ldap.user.baseDn", "ou=users,dc=example,dc=org")
+ .setProperty("ldap.group.baseDn", "ou=groups,dc=example,dc=org")
+ .setProperty("ldap.group.request",
+ "(&(objectClass=posixGroup)(memberUid={uid}))");
+
+ return settings;
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUserMappingTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUserMappingTest.java
new file mode 100644
index 00000000000..f0f1876e917
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUserMappingTest.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.auth.ldap;
+
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LdapUserMappingTest {
+
+ @Test
+ public void defaults() {
+ LdapUserMapping userMapping = new LdapUserMapping(new MapSettings(), "ldap");
+ assertThat(userMapping.getBaseDn()).isNull();
+ assertThat(userMapping.getRequest()).isEqualTo("(&(objectClass=inetOrgPerson)(uid={0}))");
+ assertThat(userMapping.getRealNameAttribute()).isEqualTo("cn");
+ assertThat(userMapping.getEmailAttribute()).isEqualTo("mail");
+
+ assertThat(userMapping.toString()).isEqualTo("LdapUserMapping{" +
+ "baseDn=null," +
+ " request=(&(objectClass=inetOrgPerson)(uid={0}))," +
+ " realNameAttribute=cn," +
+ " emailAttribute=mail}");
+ }
+
+ @Test
+ public void activeDirectory() {
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.user.baseDn", "cn=users")
+ .setProperty("ldap.user.objectClass", "user")
+ .setProperty("ldap.user.loginAttribute", "sAMAccountName");
+
+ LdapUserMapping userMapping = new LdapUserMapping(settings, "ldap");
+ LdapSearch search = userMapping.createSearch(null, "tester");
+ assertThat(search.getBaseDn()).isEqualTo("cn=users");
+ assertThat(search.getRequest()).isEqualTo("(&(objectClass=user)(sAMAccountName={0}))");
+ assertThat(search.getParameters()).isEqualTo(new String[] {"tester"});
+ assertThat(search.getReturningAttributes()).isNull();
+
+ assertThat(userMapping.toString()).isEqualTo("LdapUserMapping{" +
+ "baseDn=cn=users," +
+ " request=(&(objectClass=user)(sAMAccountName={0}))," +
+ " realNameAttribute=cn," +
+ " emailAttribute=mail}");
+ }
+
+ @Test
+ public void realm() {
+ MapSettings settings = new MapSettings()
+ .setProperty("ldap.realm", "example.org")
+ .setProperty("ldap.userObjectClass", "user")
+ .setProperty("ldap.loginAttribute", "sAMAccountName");
+
+ LdapUserMapping userMapping = new LdapUserMapping(settings, "ldap");
+ assertThat(userMapping.getBaseDn()).isEqualTo("dc=example,dc=org");
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java
new file mode 100644
index 00000000000..90e61e59fea
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.auth.ldap;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.security.UserDetails;
+import org.sonar.auth.ldap.server.LdapServer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LdapUsersProviderTest {
+ /**
+ * A reference to the original ldif file
+ */
+ public static final String USERS_EXAMPLE_ORG_LDIF = "/users.example.org.ldif";
+ /**
+ * A reference to an aditional ldif file.
+ */
+ public static final String USERS_INFOSUPPORT_COM_LDIF = "/users.infosupport.com.ldif";
+
+ @ClassRule
+ public static LdapServer exampleServer = new LdapServer(USERS_EXAMPLE_ORG_LDIF);
+ @ClassRule
+ public static LdapServer infosupportServer = new LdapServer(USERS_INFOSUPPORT_COM_LDIF, "infosupport.com", "dc=infosupport,dc=com");
+
+ @Test
+ public void test() throws Exception {
+ Settings settings = LdapSettingsFactory.generateSimpleAnonymousAccessSettings(exampleServer, infosupportServer);
+ LdapSettingsManager settingsManager = new LdapSettingsManager(settings, new LdapAutodiscovery());
+ LdapUsersProvider usersProvider = new LdapUsersProvider(settingsManager.getContextFactories(), settingsManager.getUserMappings());
+
+ UserDetails details;
+
+ details = usersProvider.getUserDetails("godin");
+ assertThat(details.getName()).isEqualTo("Evgeny Mandrikov");
+ assertThat(details.getEmail()).isEqualTo("godin@example.org");
+
+ details = usersProvider.getUserDetails("tester");
+ assertThat(details.getName()).isEqualTo("Tester Testerovich");
+ assertThat(details.getEmail()).isEqualTo("tester@example.org");
+
+ details = usersProvider.getUserDetails("without_email");
+ assertThat(details.getName()).isEqualTo("Without Email");
+ assertThat(details.getEmail()).isEqualTo("");
+
+ details = usersProvider.getUserDetails("notfound");
+ assertThat(details).isNull();
+
+ details = usersProvider.getUserDetails("robby");
+ assertThat(details.getName()).isEqualTo("Robby Developer");
+ assertThat(details.getEmail()).isEqualTo("rd@infosupport.com");
+
+ details = usersProvider.getUserDetails("testerInfo");
+ assertThat(details.getName()).isEqualTo("Tester Testerovich");
+ assertThat(details.getEmail()).isEqualTo("tester@infosupport.com");
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/ApacheDS.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/ApacheDS.java
new file mode 100644
index 00000000000..6f2f1f4d66f
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/ApacheDS.java
@@ -0,0 +1,234 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.server;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.entry.DefaultModification;
+import org.apache.directory.api.ldap.model.entry.ModificationOperation;
+import org.apache.directory.api.ldap.model.exception.LdapOperationException;
+import org.apache.directory.api.ldap.model.ldif.ChangeType;
+import org.apache.directory.api.ldap.model.ldif.LdifEntry;
+import org.apache.directory.api.ldap.model.ldif.LdifReader;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.util.FileUtils;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.InstanceLayout;
+import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
+import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
+import org.apache.directory.server.core.partition.impl.avl.AvlPartition;
+import org.apache.directory.server.kerberos.KerberosConfig;
+import org.apache.directory.server.kerberos.kdc.KdcServer;
+import org.apache.directory.server.ldap.handlers.sasl.MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
+import org.apache.directory.server.protocol.shared.transport.UdpTransport;
+import org.apache.directory.server.xdbm.impl.avl.AvlIndex;
+import org.apache.mina.util.AvailablePortFinder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class ApacheDS {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ApacheDS.class);
+
+ private final String realm;
+ private final String baseDn;
+
+ private DirectoryService directoryService;
+ private org.apache.directory.server.ldap.LdapServer ldapServer;
+ private KdcServer kdcServer;
+
+ private ApacheDS(String realm, String baseDn) {
+ this.realm = realm;
+ this.baseDn = baseDn;
+ ldapServer = new org.apache.directory.server.ldap.LdapServer();
+ }
+
+ public static ApacheDS start(String realm, String baseDn, String workDir) throws Exception {
+ return start(realm, baseDn, workDir + realm, null);
+ }
+
+ static ApacheDS start(String realm, String baseDn) throws Exception {
+ return start(realm, baseDn, "target/ldap-work/" + realm, null);
+ }
+
+ private static ApacheDS start(String realm, String baseDn, String workDir, Integer port) throws Exception {
+ return new ApacheDS(realm, baseDn)
+ .startDirectoryService(workDir)
+ .startKdcServer()
+ .startLdapServer(port == null ? AvailablePortFinder.getNextAvailable(1024) : port)
+ .activateNis();
+ }
+
+ void stop() throws Exception {
+ kdcServer.stop();
+ kdcServer = null;
+ ldapServer.stop();
+ ldapServer = null;
+ directoryService.shutdown();
+ directoryService = null;
+ }
+
+ public String getUrl() {
+ return "ldap://localhost:" + ldapServer.getPort();
+ }
+
+ /**
+ * Stream will be closed automatically.
+ */
+ public void importLdif(InputStream is) throws Exception {
+ try (LdifReader reader = new LdifReader(is)) {
+ CoreSession coreSession = directoryService.getAdminSession();
+ // see LdifFileLoader
+ for (LdifEntry ldifEntry : reader) {
+ String ldif = ldifEntry.toString();
+ LOG.info(ldif);
+ if (ChangeType.Add == ldifEntry.getChangeType() || /* assume "add" by default */ ChangeType.None == ldifEntry.getChangeType()) {
+ coreSession.add(new DefaultEntry(coreSession.getDirectoryService().getSchemaManager(), ldifEntry.getEntry()));
+ } else if (ChangeType.Modify == ldifEntry.getChangeType()) {
+ coreSession.modify(ldifEntry.getDn(), ldifEntry.getModifications());
+ } else if (ChangeType.Delete == ldifEntry.getChangeType()) {
+ coreSession.delete(ldifEntry.getDn());
+ } else {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+
+ void disableAnonymousAccess() {
+ directoryService.setAllowAnonymousAccess(false);
+ }
+
+ void enableAnonymousAccess() {
+ directoryService.setAllowAnonymousAccess(true);
+ }
+
+ private ApacheDS startDirectoryService(String workDirStr) throws Exception {
+ DefaultDirectoryServiceFactory factory = new DefaultDirectoryServiceFactory();
+ factory.init(realm);
+
+ directoryService = factory.getDirectoryService();
+ directoryService.getChangeLog().setEnabled(false);
+ directoryService.setShutdownHookEnabled(false);
+ directoryService.setAllowAnonymousAccess(true);
+
+ File workDir = new File(workDirStr);
+ if (workDir.exists()) {
+ FileUtils.deleteDirectory(workDir);
+ }
+ InstanceLayout instanceLayout = new InstanceLayout(workDir);
+ directoryService.setInstanceLayout(instanceLayout);
+
+ AvlPartition partition = new AvlPartition(directoryService.getSchemaManager());
+ partition.setId("Test");
+ partition.setSuffixDn(new Dn(directoryService.getSchemaManager(), baseDn));
+ partition.addIndexedAttributes(
+ new AvlIndex<>("ou"),
+ new AvlIndex<>("uid"),
+ new AvlIndex<>("dc"),
+ new AvlIndex<>("objectClass"));
+ partition.initialize();
+ directoryService.addPartition(partition);
+ directoryService.addLast(new KeyDerivationInterceptor());
+
+ directoryService.shutdown();
+ directoryService.startup();
+
+ return this;
+ }
+
+ private ApacheDS startLdapServer(int port) throws Exception {
+ ldapServer.setTransports(new TcpTransport(port));
+ ldapServer.setDirectoryService(directoryService);
+
+ // Setup SASL mechanisms
+ Map<String, MechanismHandler> mechanismHandlerMap = new HashMap<>();
+ mechanismHandlerMap.put(SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler());
+ mechanismHandlerMap.put(SupportedSaslMechanisms.CRAM_MD5, new CramMd5MechanismHandler());
+ mechanismHandlerMap.put(SupportedSaslMechanisms.DIGEST_MD5, new DigestMd5MechanismHandler());
+ mechanismHandlerMap.put(SupportedSaslMechanisms.GSSAPI, new GssapiMechanismHandler());
+ ldapServer.setSaslMechanismHandlers(mechanismHandlerMap);
+
+ ldapServer.setSaslHost("localhost");
+ ldapServer.setSaslRealms(Collections.singletonList(realm));
+ // TODO ldapServer.setSaslPrincipal();
+ // The base DN containing users that can be SASL authenticated.
+ ldapServer.setSearchBaseDn(baseDn);
+
+ ldapServer.start();
+
+ return this;
+ }
+
+ private ApacheDS startKdcServer() throws IOException, LdapOperationException {
+ int port = AvailablePortFinder.getNextAvailable(6088);
+
+ KerberosConfig kdcConfig = new KerberosConfig();
+ kdcConfig.setServicePrincipal("krbtgt/EXAMPLE.ORG@EXAMPLE.ORG");
+ kdcConfig.setPrimaryRealm("EXAMPLE.ORG");
+ kdcConfig.setPaEncTimestampRequired(false);
+
+ kdcServer = new KdcServer(kdcConfig);
+ kdcServer.setSearchBaseDn("dc=example,dc=org");
+ kdcServer.addTransports(new UdpTransport("localhost", port));
+ kdcServer.setDirectoryService(directoryService);
+ kdcServer.start();
+
+ FileUtils.writeStringToFile(new File("target/krb5.conf"), ""
+ + "[libdefaults]\n"
+ + " default_realm = EXAMPLE.ORG\n"
+ + "\n"
+ + "[realms]\n"
+ + " EXAMPLE.ORG = {\n"
+ + " kdc = localhost:" + port + "\n"
+ + " }\n"
+ + "\n"
+ + "[domain_realm]\n"
+ + " .example.org = EXAMPLE.ORG\n"
+ + " example.org = EXAMPLE.ORG\n",
+ StandardCharsets.UTF_8.name());
+
+ return this;
+ }
+
+ /**
+ * This seems to be required for objectClass posixGroup.
+ */
+ private ApacheDS activateNis() throws Exception {
+ directoryService.getAdminSession().modify(
+ new Dn("cn=nis,ou=schema"),
+ new DefaultModification(ModificationOperation.REPLACE_ATTRIBUTE, "m-disabled", "FALSE"));
+ return this;
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/LdapServer.java b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/LdapServer.java
new file mode 100644
index 00000000000..6539ac5a662
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/LdapServer.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.server;
+
+import org.junit.rules.ExternalResource;
+
+public class LdapServer extends ExternalResource {
+
+ private ApacheDS server;
+ private String ldif;
+ private final String realm;
+ private final String baseDn;
+
+ public LdapServer(String ldifResourceName) {
+ this(ldifResourceName, "example.org", "dc=example,dc=org");
+ }
+
+ public LdapServer(String ldifResourceName, String realm, String baseDn) {
+ this.ldif = ldifResourceName;
+ this.realm = realm;
+ this.baseDn = baseDn;
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ server = ApacheDS.start(realm, baseDn);
+ server.importLdif(LdapServer.class.getResourceAsStream(ldif));
+ }
+
+ @Override
+ protected void after() {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public String getUrl() {
+ return server.getUrl();
+ }
+
+ public void disableAnonymousAccess() {
+ server.disableAnonymousAccess();
+ }
+
+ public void enableAnonymousAccess() {
+ server.enableAnonymousAccess();
+ }
+
+}
diff --git a/server/sonar-auth-ldap/src/test/resources/conf/krb5.conf b/server/sonar-auth-ldap/src/test/resources/conf/krb5.conf
new file mode 100644
index 00000000000..04fd9f9423a
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/conf/krb5.conf
@@ -0,0 +1,20 @@
+[libdefaults]
+ default_realm = EXAMPLE.ORG
+
+[realms]
+ EXAMPLE.ORG = {
+ kdc = localhost:6088
+ }
+ INFOSUPPORT.COM = {
+ kdc = localhost:6089
+ }
+
+[domain_realm]
+ .example.org = EXAMPLE.ORG
+ example.org = EXAMPLE.ORG
+ .infosupport.com = INFOSUPPORT.COM
+ infosupport.com = INFOSUPPORT.COM
+
+[login]
+ krb4_convert = true
+ krb4_get_tickets = false
diff --git a/server/sonar-auth-ldap/src/test/resources/conf/sasl_mech.properties b/server/sonar-auth-ldap/src/test/resources/conf/sasl_mech.properties
new file mode 100644
index 00000000000..f3d209e335c
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/conf/sasl_mech.properties
@@ -0,0 +1,23 @@
+#
+# SonarQube
+# Copyright (C) 2009-2019 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.
+#
+ldap.url:ldap://localhost:1024
+# TODO don't work as expected
+ldap.authentication:DIGEST-MD5 CRAM-MD5
+#ldap.realm: example.org
diff --git a/server/sonar-auth-ldap/src/test/resources/krb.ldif b/server/sonar-auth-ldap/src/test/resources/krb.ldif
new file mode 100644
index 00000000000..6c8235dc91e
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/krb.ldif
@@ -0,0 +1,55 @@
+dn: dc=example,dc=org
+dc: example
+objectClass: domain
+objectClass: top
+
+dn: ou=Users,dc=example,dc=org
+objectClass: organizationalUnit
+objectClass: top
+ou: Users
+
+dn: uid=krbtgt,ou=Users,dc=example,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: KDC Service
+sn: Service
+uid: krbtgt
+userPassword: secret
+krb5PrincipalName: krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
+krb5KeyVersionNumber: 0
+
+dn: cn=SonarQube,ou=Users,dc=example,dc=org
+objectClass: top
+objectClass: organizationalRole
+objectClass: simpleSecurityObject
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: SonarQube
+userPassword: bind_password
+krb5PrincipalName: SonarQube@EXAMPLE.ORG
+krb5KeyVersionNumber: 0
+
+dn: uid=godin,ou=Users,dc=example,dc=org
+objectClass: top
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: Evgeny Mandrikov
+sn: Mandrikov
+uid: godin
+userPassword: user_password
+krb5PrincipalName: Godin@EXAMPLE.ORG
+krb5KeyVersionNumber: 0
+
+dn: ou=Groups,dc=example,dc=org
+objectclass:organizationalunit
+ou: groups
+
+dn: cn=sonar-users,ou=Groups,dc=example,dc=org
+objectclass: groupOfUniqueNames
+cn: sonar-users
+uniqueMember: uid=godin,ou=Users,dc=example,dc=org
diff --git a/server/sonar-auth-ldap/src/test/resources/logback-test.xml b/server/sonar-auth-ldap/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..cc9519d939a
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/logback-test.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ SonarQube
+ ~ Copyright (C) 2009-2019 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.
+ -->
+
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>
+ %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.sonar">
+ <level value="DEBUG"/>
+ </logger>
+
+ <logger name="org.apache.directory">
+ <level value="ERROR"/>
+ </logger>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration>
diff --git a/server/sonar-auth-ldap/src/test/resources/static-groups.example.org.ldif b/server/sonar-auth-ldap/src/test/resources/static-groups.example.org.ldif
new file mode 100644
index 00000000000..857efc7c508
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/static-groups.example.org.ldif
@@ -0,0 +1,81 @@
+dn: dc=example,dc=org
+objectClass: domain
+objectClass: extensibleObject
+objectClass: top
+dc: example
+
+#
+# USERS
+#
+
+dn: ou=users,dc=example,dc=org
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+# Bind user
+dn: cn=bind,ou=users,dc=example,dc=org
+objectClass: organizationalRole
+objectClass: simpleSecurityObject
+objectClass: top
+cn: bind
+userpassword: bindpassword
+
+# Typical user
+dn: cn=Evgeny Mandrikov,ou=users,dc=example,dc=org
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Evgeny Mandrikov
+sn: Mandrikov
+givenname: Evgeny
+mail: godin@example.org
+uid: godin
+userpassword: secret1
+
+# Just one more user
+dn: cn=Tester Testerovich,ou=users,dc=example,dc=org
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Tester Testerovich
+givenname: Tester
+sn: Testerovich
+mail: tester@example.org
+uid: tester
+userpassword: secret2
+
+#
+# GROUPS
+#
+
+dn: ou=groups,dc=example,dc=org
+objectclass:organizationalunit
+ou: groups
+
+# sonar-users
+dn: cn=sonar-users,ou=groups,dc=example,dc=org
+objectclass: groupOfUniqueNames
+cn: sonar-users
+uniqueMember: cn=Tester Testerovich,ou=users,dc=example,dc=org
+uniqueMember: cn=Evgeny Mandrikov,ou=users,dc=example,dc=org
+
+# sonar-developers
+dn: cn=sonar-developers,ou=groups,dc=example,dc=org
+objectclass: groupOfUniqueNames
+cn: sonar-developers
+uniqueMember: cn=Evgeny Mandrikov,ou=users,dc=example,dc=org
+
+# linux-users
+dn: cn=linux-users,ou=groups,dc=example,dc=org
+objectclass: posixGroup
+objectclass: top
+cn: linux-users
+gidNumber: 10000
+memberUid: godin
diff --git a/server/sonar-auth-ldap/src/test/resources/users-apacheds.ldif b/server/sonar-auth-ldap/src/test/resources/users-apacheds.ldif
new file mode 100644
index 00000000000..d0231512459
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/users-apacheds.ldif
@@ -0,0 +1,88 @@
+dn: dc=example,dc=org
+objectClass: domain
+objectClass: extensibleObject
+objectClass: top
+dc: example
+
+#
+# USERS
+#
+
+dn: ou=users,dc=example,dc=org
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+dn: cn=bind,ou=users,dc=example,dc=org
+objectClass: organizationalRole
+objectClass: uidObject
+objectClass: simpleSecurityObject
+objectClass: top
+cn: bind
+uid: sonar
+userpassword: bindpassword
+
+dn: cn=Evgeny Mandrikov,ou=users,dc=example,dc=org
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: Evgeny Mandrikov
+givenname: Evgeny
+mail: godin@example.org
+sn: Mandrikov
+uid: godin
+userpassword: secret1
+krb5PrincipalName: godin@EXAMPLE.ORG
+krb5KeyVersionNumber: 0
+
+dn: cn=Tester Testerovich,ou=users,dc=example,dc=org
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+objectClass: krb5principal
+objectClass: krb5kdcentry
+cn: Tester Testerovich
+givenname: Tester
+mail: tester@example.org
+sn: Testerovich
+uid: tester
+userpassword: secret2
+krb5PrincipalName: tester@EXAMPLE.ORG
+krb5KeyVersionNumber: 0
+
+####
+# For Krb5
+####
+dn: uid=krbtgt,ou=users,dc=example,dc=org
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+objectClass: krb5principal
+objectClass: krb5kdcentry
+sn: Service
+cn: KDC Service
+uid: krbtgt
+userPassword: secret
+krb5PrincipalName: krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
+krb5KeyVersionNumber: 0
+
+dn: uid=ldap,ou=users,dc=example,dc=org
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+objectClass: krb5principal
+objectClass: krb5kdcentry
+sn: Service
+cn: LDAP Service
+uid: ldap
+userPassword: randall
+krb5PrincipalName: ldap/localhost@EXAMPLE.COM
+krb5KeyVersionNumber: 0
diff --git a/server/sonar-auth-ldap/src/test/resources/users.example.org.ldif b/server/sonar-auth-ldap/src/test/resources/users.example.org.ldif
new file mode 100644
index 00000000000..3dc462afbb0
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/users.example.org.ldif
@@ -0,0 +1,98 @@
+dn: dc=example,dc=org
+objectClass: domain
+objectClass: extensibleObject
+objectClass: top
+dc: example
+
+#
+# USERS
+#
+
+dn: ou=users,dc=example,dc=org
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+# Bind user
+dn: cn=bind,ou=users,dc=example,dc=org
+objectClass: organizationalRole
+objectClass: uidObject
+objectClass: simpleSecurityObject
+objectClass: top
+cn: bind
+uid: sonar
+userpassword: bindpassword
+
+# Typical user
+dn: cn=Evgeny Mandrikov,ou=users,dc=example,dc=org
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Evgeny Mandrikov
+givenname: Evgeny
+sn: Mandrikov
+mail: godin@example.org
+uid: godin
+userpassword: secret1
+
+# Just one more user
+dn: cn=Tester Testerovich,ou=users,dc=example,dc=org
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Tester Testerovich
+givenname: Tester
+sn: Testerovich
+mail: tester@example.org
+uid: tester
+userpassword: secret2
+
+# Special case which can cause NPE
+dn: cn=Without Email,ou=users,dc=example,dc=org
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Without Email
+givenname: Without
+sn: Email
+uid: without_email
+userpassword: secret3
+
+
+#
+# GROUPS
+#
+
+dn: ou=groups,dc=example,dc=org
+objectclass:organizationalunit
+ou: groups
+
+# sonar-users
+dn: cn=sonar-users,ou=groups,dc=example,dc=org
+objectclass: groupOfUniqueNames
+cn: sonar-users
+uniqueMember: cn=Tester Testerovich,ou=users,dc=example,dc=org
+uniqueMember: cn=Evgeny Mandrikov,ou=users,dc=example,dc=org
+
+# sonar-developers
+dn: cn=sonar-developers,ou=groups,dc=example,dc=org
+objectclass: groupOfUniqueNames
+cn: sonar-developers
+uniqueMember: cn=Evgeny Mandrikov,ou=users,dc=example,dc=org
+
+# linux-users
+dn: cn=linux-users,ou=groups,dc=example,dc=org
+objectclass: posixGroup
+objectclass: top
+cn: linux-users
+gidNumber: 10000
+memberUid: godin \ No newline at end of file
diff --git a/server/sonar-auth-ldap/src/test/resources/users.infosupport.com.ldif b/server/sonar-auth-ldap/src/test/resources/users.infosupport.com.ldif
new file mode 100644
index 00000000000..a08174bf72b
--- /dev/null
+++ b/server/sonar-auth-ldap/src/test/resources/users.infosupport.com.ldif
@@ -0,0 +1,98 @@
+dn: dc=infosupport,dc=com
+objectClass: domain
+objectClass: extensibleObject
+objectClass: top
+dc: infosupport
+
+#
+# USERS
+#
+
+dn: ou=users,dc=infosupport,dc=com
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+# Bind user
+dn: cn=bind,ou=users,dc=infosupport,dc=com
+objectClass: organizationalRole
+objectClass: uidObject
+objectClass: simpleSecurityObject
+objectClass: top
+cn: bind
+uid: sonar
+userpassword: bindpassword
+
+# Typical user
+dn: cn=Robby Developer,ou=users,dc=infosupport,dc=com
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Robby Developer
+givenname: Robby
+sn: Developer
+mail: rd@infosupport.com
+uid: robby
+userpassword: secret1
+
+# Just one more user
+dn: cn=Tester Testerovich,ou=users,dc=infosupport,dc=com
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Tester Testerovich
+givenname: Tester
+sn: Testerovich
+mail: tester@infosupport.com
+uid: testerInfo
+userpassword: secret2
+
+# Special case which can cause NPE
+dn: cn=Without Email,ou=users,dc=infosupport,dc=com
+objectClass: organizationalPerson
+objectClass: person
+objectClass: extensibleObject
+objectClass: uidObject
+objectClass: inetOrgPerson
+objectClass: top
+cn: Without Email
+givenname: Without
+sn: Email
+uid: without_email
+userpassword: secret3
+
+
+#
+# GROUPS
+#
+
+dn: ou=groups,dc=infosupport,dc=com
+objectclass:organizationalunit
+ou: groups
+
+# sonar-users
+dn: cn=sonar-users,ou=groups,dc=infosupport,dc=com
+objectclass: groupOfUniqueNames
+cn: sonar-users
+uniqueMember: cn=Robby Developer,ou=users,dc=infosupport,dc=com
+uniqueMember: cn=Tester Testerovich,ou=users,dc=infosupport,dc=com
+
+# sonar-developers
+dn: cn=sonar-developers,ou=groups,dc=infosupport,dc=com
+objectclass: groupOfUniqueNames
+cn: sonar-developers
+uniqueMember: cn=Robby Developer,ou=users,dc=infosupport,dc=com
+
+# linux-users
+dn: cn=linux-users,ou=groups,dc=infosupport,dc=com
+objectclass: posixGroup
+objectclass: top
+cn: linux-users
+gidNumber: 10000
+memberUid: robby \ No newline at end of file
diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
index 507b2df8882..7ec3cf1a150 100644
--- a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
+++ b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
@@ -57,7 +57,7 @@ public class SamlIdentityProviderTest {
setSettings(true);
assertThat(underTest.getKey()).isEqualTo("saml");
assertThat(underTest.getName()).isEqualTo("SAML");
- assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/static/authsaml/saml.png");
+ assertThat(underTest.getDisplay().getIconPath()).isEqualTo("/images/saml.png");
assertThat(underTest.getDisplay().getBackgroundColor()).isEqualTo("#444444");
assertThat(underTest.allowsUsersToSignUp()).isTrue();
}
diff --git a/server/sonar-docs/src/pages/instance-administration/delegated-auth.md b/server/sonar-docs/src/pages/instance-administration/delegated-auth.md
index 3ccac89cf18..48c5abc41c5 100644
--- a/server/sonar-docs/src/pages/instance-administration/delegated-auth.md
+++ b/server/sonar-docs/src/pages/instance-administration/delegated-auth.md
@@ -167,7 +167,7 @@ GSSAPI|![](/images/check.svg)| | |
![](/images/check.svg) = successfully tested
### Setup
-1. Configure the LDAP plugin by editing _$SONARQUBE-HOME/conf/sonar.properties_ (see table below)
+1. Configure LDAP by editing _$SONARQUBE-HOME/conf/sonar.properties_ (see table below)
2. Restart the SonarQube server and check the log file for:
```
INFO org.sonar.INFO Security realm: LDAP ...
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java b/server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
index 63254f0553c..c967c8a6be4 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
@@ -24,9 +24,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import java.lang.annotation.Annotation;
import java.util.Collection;
-import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import java.util.stream.Collectors;
import org.sonar.api.ExtensionProvider;
import org.sonar.api.Plugin;
@@ -39,6 +39,7 @@ import org.sonar.core.platform.ComponentContainer;
import org.sonar.core.platform.PluginInfo;
import org.sonar.core.platform.PluginRepository;
+import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.sonar.core.extension.ExtensionProviderSupport.isExtensionProvider;
@@ -47,7 +48,7 @@ import static org.sonar.core.extension.ExtensionProviderSupport.isExtensionProvi
*/
public abstract class ServerExtensionInstaller {
- private static final Set<String> NO_MORE_COMPATIBLE_PLUGINS = ImmutableSet.of("authgithub", "authgitlab", "authsaml");
+ private static final Set<String> NO_MORE_COMPATIBLE_PLUGINS = ImmutableSet.of("authgithub", "authgitlab", "authsaml", "ldap");
private final SonarRuntime sonarRuntime;
private final PluginRepository pluginRepository;
@@ -84,7 +85,7 @@ public abstract class ServerExtensionInstaller {
}
} catch (Throwable e) {
// catch Throwable because we want to catch Error too (IncompatibleClassChangeError, ...)
- throw new IllegalStateException(String.format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e);
+ throw new IllegalStateException(format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e);
}
}
for (Map.Entry<PluginInfo, Object> entry : installedExtensionsByPlugin.entries()) {
@@ -97,19 +98,19 @@ public abstract class ServerExtensionInstaller {
}
} catch (Throwable e) {
// catch Throwable because we want to catch Error too (IncompatibleClassChangeError, ...)
- throw new IllegalStateException(String.format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e);
+ throw new IllegalStateException(format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e);
}
}
}
private void failWhenNoMoreCompatiblePlugins() {
- List<String> noMoreCompatiblePluginNames = pluginRepository.getPluginInfos()
+ Set<String> noMoreCompatiblePluginNames = pluginRepository.getPluginInfos()
.stream()
.filter(pluginInfo -> NO_MORE_COMPATIBLE_PLUGINS.contains(pluginInfo.getKey()))
.map(PluginInfo::getName)
- .collect(Collectors.toList());
+ .collect(Collectors.toCollection(TreeSet::new));
if (!noMoreCompatiblePluginNames.isEmpty()) {
- throw MessageException.of(String.format("Plugins '%s' are no more compatible with SonarQube", String.join(",", noMoreCompatiblePluginNames)));
+ throw MessageException.of(format("Plugins '%s' are no more compatible with SonarQube", String.join(", ", noMoreCompatiblePluginNames)));
}
}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java
index d5be9ce92aa..90250ae7f0d 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java
@@ -77,13 +77,14 @@ public class ServerExtensionInstallerTest {
}
@Test
- public void fail_when_detecting_gitlab_auth_plugin() {
- PluginInfo foo = newPlugin("authgitlab", "GitLab Auth");
- pluginRepository.add(foo, mock(Plugin.class));
+ public void fail_when_detecting_auth_plugins() {
+ pluginRepository.add(newPlugin("authgitlab", "GitLab Auth"), mock(Plugin.class));
+ pluginRepository.add(newPlugin("authsaml", "SAML Auth"), mock(Plugin.class));
+ pluginRepository.add(newPlugin("ldap", "LDAP"), mock(Plugin.class));
ComponentContainer componentContainer = new ComponentContainer();
expectedException.expect(MessageException.class);
- expectedException.expectMessage("Plugins 'GitLab Auth' are no more compatible with SonarQube");
+ expectedException.expectMessage("Plugins 'GitLab Auth, LDAP, SAML Auth' are no more compatible with SonarQube");
underTest.installExtensions(componentContainer);
}
@@ -100,6 +101,18 @@ public class ServerExtensionInstallerTest {
underTest.installExtensions(componentContainer);
}
+ @Test
+ public void fail_when_detecting_ldap_auth_plugin() {
+ PluginInfo foo = newPlugin("ldap", "LDAP");
+ pluginRepository.add(foo, mock(Plugin.class));
+ ComponentContainer componentContainer = new ComponentContainer();
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Plugins 'LDAP' are no more compatible with SonarQube");
+
+ underTest.installExtensions(componentContainer);
+ }
+
private static PluginInfo newPlugin(String key, String name) {
PluginInfo plugin = mock(PluginInfo.class);
when(plugin.getKey()).thenReturn(key);
diff --git a/server/sonar-webserver/build.gradle b/server/sonar-webserver/build.gradle
index b502aa97e21..2a38762339a 100644
--- a/server/sonar-webserver/build.gradle
+++ b/server/sonar-webserver/build.gradle
@@ -14,6 +14,7 @@ dependencies {
compile project(':sonar-core')
compile project(':server:sonar-auth-github')
compile project(':server:sonar-auth-gitlab')
+ compile project(':server:sonar-auth-ldap')
compile project(':server:sonar-auth-saml')
compile project(':server:sonar-ce-task-projectanalysis')
compile project(':server:sonar-process')
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 39f3f44cb34..47176b4daf6 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -30,6 +30,7 @@ import org.sonar.api.rules.XMLRuleParser;
import org.sonar.api.server.rule.RulesDefinitionXmlLoader;
import org.sonar.auth.github.GitHubModule;
import org.sonar.auth.gitlab.GitLabModule;
+import org.sonar.auth.ldap.LdapModule;
import org.sonar.auth.saml.SamlModule;
import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationModule;
import org.sonar.ce.task.projectanalysis.taskprocessor.ReportTaskProcessor;
@@ -355,6 +356,7 @@ public class PlatformLevel4 extends PlatformLevel {
AuthenticationWsModule.class,
GitHubModule.class,
GitLabModule.class,
+ LdapModule.class,
SamlModule.class,
// users
diff --git a/settings.gradle b/settings.gradle
index 588990d2772..a10bfa219d0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -5,6 +5,7 @@ include 'plugins:sonar-xoo-plugin'
include 'server:sonar-auth-common'
include 'server:sonar-auth-github'
include 'server:sonar-auth-gitlab'
+include 'server:sonar-auth-ldap'
include 'server:sonar-auth-saml'
include 'server:sonar-ce'
include 'server:sonar-ce-common'
@@ -38,6 +39,7 @@ include 'sonar-scanner-engine-shaded'
include 'sonar-scanner-protocol'
include 'sonar-shutdowner'
include 'sonar-testing-harness'
+include 'sonar-testing-ldap'
include 'sonar-ws'
include 'sonar-ws-generator'
diff --git a/sonar-application/build.gradle b/sonar-application/build.gradle
index b233360ed0d..939a4fd5e47 100644
--- a/sonar-application/build.gradle
+++ b/sonar-application/build.gradle
@@ -59,7 +59,6 @@ dependencies {
bundledPlugin 'org.sonarsource.java:sonar-java-plugin@jar'
bundledPlugin 'org.sonarsource.jacoco:sonar-jacoco-plugin:1.0.2.475@jar'
bundledPlugin 'org.sonarsource.javascript:sonar-javascript-plugin@jar'
- bundledPlugin 'org.sonarsource.ldap:sonar-ldap-plugin:2.2.0.608@jar'
bundledPlugin 'org.sonarsource.php:sonar-php-plugin@jar'
bundledPlugin 'org.sonarsource.python:sonar-python-plugin@jar'
bundledPlugin "org.sonarsource.slang:sonar-kotlin-plugin@jar"
diff --git a/sonar-testing-ldap/build.gradle b/sonar-testing-ldap/build.gradle
new file mode 100644
index 00000000000..4a2fe9a4ad7
--- /dev/null
+++ b/sonar-testing-ldap/build.gradle
@@ -0,0 +1,15 @@
+sonarqube {
+ properties {
+ property 'sonar.projectName', "${projectTitle} :: LDAP Testing"
+ }
+}
+
+dependencies {
+ compile 'junit:junit'
+ compile 'org.apache.directory.server:apacheds-all:2.0.0-M24'
+ compile 'org.slf4j:slf4j-api:1.7.12'
+
+ testCompile 'org.assertj:assertj-core'
+ testCompile 'org.hamcrest:hamcrest-core'
+ testCompile 'org.mockito:mockito-core'
+}
diff --git a/sonar-testing-ldap/src/main/java/org/sonar/ldap/ApacheDS.java b/sonar-testing-ldap/src/main/java/org/sonar/ldap/ApacheDS.java
new file mode 100644
index 00000000000..cfcb880dee0
--- /dev/null
+++ b/sonar-testing-ldap/src/main/java/org/sonar/ldap/ApacheDS.java
@@ -0,0 +1,240 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.ldap;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.entry.DefaultModification;
+import org.apache.directory.api.ldap.model.entry.ModificationOperation;
+import org.apache.directory.api.ldap.model.exception.LdapOperationException;
+import org.apache.directory.api.ldap.model.ldif.ChangeType;
+import org.apache.directory.api.ldap.model.ldif.LdifEntry;
+import org.apache.directory.api.ldap.model.ldif.LdifReader;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.util.FileUtils;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.InstanceLayout;
+import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
+import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
+import org.apache.directory.server.core.partition.impl.avl.AvlPartition;
+import org.apache.directory.server.kerberos.KerberosConfig;
+import org.apache.directory.server.kerberos.kdc.KdcServer;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.ldap.handlers.sasl.MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
+import org.apache.directory.server.protocol.shared.transport.UdpTransport;
+import org.apache.directory.server.xdbm.impl.avl.AvlIndex;
+import org.apache.mina.util.AvailablePortFinder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class ApacheDS {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ApacheDS.class);
+
+ private final String realm;
+ private final String baseDn;
+
+ private DirectoryService directoryService;
+ private LdapServer ldapServer;
+ private KdcServer kdcServer;
+
+ private ApacheDS(String realm, String baseDn) {
+ this.realm = realm;
+ this.baseDn = baseDn;
+ ldapServer = new LdapServer();
+ }
+
+ public static ApacheDS start(String realm, String baseDn, String workDir, Integer port) throws Exception {
+ return new ApacheDS(realm, baseDn)
+ .startDirectoryService(workDir)
+ .startKdcServer()
+ .startLdapServer(port == null ? AvailablePortFinder.getNextAvailable(1024) : port)
+ .activateNis();
+ }
+
+ public static ApacheDS start(String realm, String baseDn, String workDir) throws Exception {
+ return start(realm, baseDn, workDir + realm, null);
+ }
+
+ public static ApacheDS start(String realm, String baseDn) throws Exception {
+ return start(realm, baseDn, "target/ldap-work/" + realm, null);
+ }
+
+ public void stop() {
+ try {
+ kdcServer.stop();
+ kdcServer = null;
+ ldapServer.stop();
+ ldapServer = null;
+ directoryService.shutdown();
+ directoryService = null;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public String getUrl() {
+ return "ldap://localhost:" + ldapServer.getPort();
+ }
+
+ /**
+ * Stream will be closed automatically.
+ */
+ public void importLdif(InputStream is) throws Exception {
+ try (LdifReader reader = new LdifReader(is)) {
+ CoreSession coreSession = directoryService.getAdminSession();
+ // see LdifFileLoader
+ for (LdifEntry ldifEntry : reader) {
+ String ldif = ldifEntry.toString();
+ LOG.info(ldif);
+ if (ChangeType.Add == ldifEntry.getChangeType() || /* assume "add" by default */ ChangeType.None == ldifEntry.getChangeType()) {
+ coreSession.add(new DefaultEntry(coreSession.getDirectoryService().getSchemaManager(), ldifEntry.getEntry()));
+ } else if (ChangeType.Modify == ldifEntry.getChangeType()) {
+ coreSession.modify(ldifEntry.getDn(), ldifEntry.getModifications());
+ } else if (ChangeType.Delete == ldifEntry.getChangeType()) {
+ coreSession.delete(ldifEntry.getDn());
+ } else {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+
+ public void disableAnonymousAccess() {
+ directoryService.setAllowAnonymousAccess(false);
+ }
+
+ public void enableAnonymousAccess() {
+ directoryService.setAllowAnonymousAccess(true);
+ }
+
+ private ApacheDS startDirectoryService(String workDirStr) throws Exception {
+ DefaultDirectoryServiceFactory factory = new DefaultDirectoryServiceFactory();
+ factory.init(realm);
+
+ directoryService = factory.getDirectoryService();
+ directoryService.getChangeLog().setEnabled(false);
+ directoryService.setShutdownHookEnabled(false);
+ directoryService.setAllowAnonymousAccess(true);
+
+ File workDir = new File(workDirStr);
+ if (workDir.exists()) {
+ FileUtils.deleteDirectory(workDir);
+ }
+ InstanceLayout instanceLayout = new InstanceLayout(workDir);
+ directoryService.setInstanceLayout(instanceLayout);
+
+ AvlPartition partition = new AvlPartition(directoryService.getSchemaManager());
+ partition.setId("Test");
+ partition.setSuffixDn(new Dn(directoryService.getSchemaManager(), baseDn));
+ partition.addIndexedAttributes(
+ new AvlIndex<>("ou"),
+ new AvlIndex<>("uid"),
+ new AvlIndex<>("dc"),
+ new AvlIndex<>("objectClass"));
+ partition.initialize();
+ directoryService.addPartition(partition);
+ directoryService.addLast(new KeyDerivationInterceptor());
+
+ directoryService.shutdown();
+ directoryService.startup();
+
+ return this;
+ }
+
+ private ApacheDS startLdapServer(int port) throws Exception {
+ ldapServer.setTransports(new TcpTransport(port));
+ ldapServer.setDirectoryService(directoryService);
+
+ // Setup SASL mechanisms
+ Map<String, MechanismHandler> mechanismHandlerMap = new HashMap<>();
+ mechanismHandlerMap.put(SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler());
+ mechanismHandlerMap.put(SupportedSaslMechanisms.CRAM_MD5, new CramMd5MechanismHandler());
+ mechanismHandlerMap.put(SupportedSaslMechanisms.DIGEST_MD5, new DigestMd5MechanismHandler());
+ mechanismHandlerMap.put(SupportedSaslMechanisms.GSSAPI, new GssapiMechanismHandler());
+ ldapServer.setSaslMechanismHandlers(mechanismHandlerMap);
+
+ ldapServer.setSaslHost("localhost");
+ ldapServer.setSaslRealms(Collections.singletonList(realm));
+ // TODO ldapServer.setSaslPrincipal();
+ // The base DN containing users that can be SASL authenticated.
+ ldapServer.setSearchBaseDn(baseDn);
+
+ ldapServer.start();
+
+ return this;
+ }
+
+ private ApacheDS startKdcServer() throws IOException, LdapOperationException {
+ int port = AvailablePortFinder.getNextAvailable(6088);
+
+ KerberosConfig kdcConfig = new KerberosConfig();
+ kdcConfig.setServicePrincipal("krbtgt/EXAMPLE.ORG@EXAMPLE.ORG");
+ kdcConfig.setPrimaryRealm("EXAMPLE.ORG");
+ kdcConfig.setPaEncTimestampRequired(false);
+
+ kdcServer = new KdcServer(kdcConfig);
+ kdcServer.setSearchBaseDn("dc=example,dc=org");
+ kdcServer.addTransports(new UdpTransport("localhost", port));
+ kdcServer.setDirectoryService(directoryService);
+ kdcServer.start();
+
+ FileUtils.writeStringToFile(new File("target/krb5.conf"), ""
+ + "[libdefaults]\n"
+ + " default_realm = EXAMPLE.ORG\n"
+ + "\n"
+ + "[realms]\n"
+ + " EXAMPLE.ORG = {\n"
+ + " kdc = localhost:" + port + "\n"
+ + " }\n"
+ + "\n"
+ + "[domain_realm]\n"
+ + " .example.org = EXAMPLE.ORG\n"
+ + " example.org = EXAMPLE.ORG\n",
+ StandardCharsets.UTF_8.name());
+
+ return this;
+ }
+
+ /**
+ * This seems to be required for objectClass posixGroup.
+ */
+ private ApacheDS activateNis() throws Exception {
+ directoryService.getAdminSession().modify(
+ new Dn("cn=nis,ou=schema"),
+ new DefaultModification(ModificationOperation.REPLACE_ATTRIBUTE, "m-disabled", "FALSE"));
+ return this;
+ }
+
+}
diff --git a/sonar-testing-ldap/src/main/java/org/sonar/ldap/package-info.java b/sonar-testing-ldap/src/main/java/org/sonar/ldap/package-info.java
new file mode 100644
index 00000000000..50daadb290a
--- /dev/null
+++ b/sonar-testing-ldap/src/main/java/org/sonar/ldap/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.ldap;
diff --git a/sonar-testing-ldap/src/test/java/org/sonar/ldap/ApacheDSTest.java b/sonar-testing-ldap/src/test/java/org/sonar/ldap/ApacheDSTest.java
new file mode 100644
index 00000000000..a193f7bb7e3
--- /dev/null
+++ b/sonar-testing-ldap/src/test/java/org/sonar/ldap/ApacheDSTest.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.ldap;
+
+import org.junit.Test;
+
+public class ApacheDSTest {
+
+ @Test
+ public void start_and_stop_apache_server() throws Exception {
+ ApacheDS apacheDS = ApacheDS.start("example.org", "dc=example,dc=org");
+ apacheDS.importLdif(ApacheDS.class.getResourceAsStream("/init.ldif"));
+ apacheDS.importLdif(ApacheDS.class.getResourceAsStream("/change.ldif"));
+ apacheDS.importLdif(ApacheDS.class.getResourceAsStream("/delete.ldif"));
+ apacheDS.disableAnonymousAccess();
+ apacheDS.enableAnonymousAccess();
+ apacheDS.stop();
+ }
+
+}
diff --git a/sonar-testing-ldap/src/test/resources/change.ldif b/sonar-testing-ldap/src/test/resources/change.ldif
new file mode 100644
index 00000000000..903ebc0e1f7
--- /dev/null
+++ b/sonar-testing-ldap/src/test/resources/change.ldif
@@ -0,0 +1,5 @@
+dn: cn=Evgeny Mandrikov,dc=example,dc=org
+changetype: modify
+replace: userpassword
+userpassword: 54321
+-
diff --git a/sonar-testing-ldap/src/test/resources/delete.ldif b/sonar-testing-ldap/src/test/resources/delete.ldif
new file mode 100644
index 00000000000..586472648cd
--- /dev/null
+++ b/sonar-testing-ldap/src/test/resources/delete.ldif
@@ -0,0 +1,2 @@
+dn: cn=Evgeny Mandrikov,dc=example,dc=org
+changetype: delete
diff --git a/sonar-testing-ldap/src/test/resources/init.ldif b/sonar-testing-ldap/src/test/resources/init.ldif
new file mode 100644
index 00000000000..4c3ce81f0b4
--- /dev/null
+++ b/sonar-testing-ldap/src/test/resources/init.ldif
@@ -0,0 +1,9 @@
+dn: dc=example,dc=org
+objectClass: domain
+objectClass: top
+dc: example
+
+dn: cn=Evgeny Mandrikov,dc=example,dc=org
+objectClass: inetOrgPerson
+cn: Evgeny Mandrikov
+sn: Mandrikov
diff --git a/sonar-testing-ldap/src/test/resources/logback-test.xml b/sonar-testing-ldap/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..f0b4f642ca5
--- /dev/null
+++ b/sonar-testing-ldap/src/test/resources/logback-test.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ SonarQube
+ ~ Copyright (C) 2009-2019 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.
+ -->
+
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>
+ %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <logger name="org.apache">
+ <level value="ERROR"/>
+ </logger>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration>