diff options
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> |