Browse Source

SONAR-12471 Embed LDAP authentication

tags/8.0
Julien Lancelot 4 years ago
parent
commit
274c9faaf5
60 changed files with 4603 additions and 14 deletions
  1. 37
    0
      .cirrus.yml
  2. 22
    0
      server/sonar-auth-ldap/build.gradle
  3. 57
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/CallbackHandlerImpl.java
  4. 82
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/ContextHelper.java
  5. 61
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/Krb5LoginConfiguration.java
  6. 130
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java
  7. 165
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAutodiscovery.java
  8. 247
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapContextFactory.java
  9. 32
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapException.java
  10. 152
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupMapping.java
  11. 144
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java
  12. 34
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapModule.java
  13. 82
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java
  14. 191
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSearch.java
  15. 195
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSettingsManager.java
  16. 135
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserMapping.java
  17. 125
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java
  18. 23
    0
      server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/package-info.java
  19. 48
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/CallbackHandlerImplTest.java
  20. 53
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/ContextHelperTest.java
  21. 84
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java
  22. 129
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java
  23. 100
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java
  24. 93
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutodiscoveryTest.java
  25. 65
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupMappingTest.java
  26. 149
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java
  27. 38
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapModuleTest.java
  28. 82
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java
  29. 69
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapReferralsTest.java
  30. 118
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSearchTest.java
  31. 95
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsFactory.java
  32. 201
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsManagerTest.java
  33. 76
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUserMappingTest.java
  34. 77
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java
  35. 234
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/ApacheDS.java
  36. 68
    0
      server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/LdapServer.java
  37. 20
    0
      server/sonar-auth-ldap/src/test/resources/conf/krb5.conf
  38. 23
    0
      server/sonar-auth-ldap/src/test/resources/conf/sasl_mech.properties
  39. 55
    0
      server/sonar-auth-ldap/src/test/resources/krb.ldif
  40. 45
    0
      server/sonar-auth-ldap/src/test/resources/logback-test.xml
  41. 81
    0
      server/sonar-auth-ldap/src/test/resources/static-groups.example.org.ldif
  42. 88
    0
      server/sonar-auth-ldap/src/test/resources/users-apacheds.ldif
  43. 98
    0
      server/sonar-auth-ldap/src/test/resources/users.example.org.ldif
  44. 98
    0
      server/sonar-auth-ldap/src/test/resources/users.infosupport.com.ldif
  45. 1
    1
      server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
  46. 1
    1
      server/sonar-docs/src/pages/instance-administration/delegated-auth.md
  47. 8
    7
      server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
  48. 17
    4
      server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java
  49. 1
    0
      server/sonar-webserver/build.gradle
  50. 2
    0
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  51. 2
    0
      settings.gradle
  52. 0
    1
      sonar-application/build.gradle
  53. 15
    0
      sonar-testing-ldap/build.gradle
  54. 240
    0
      sonar-testing-ldap/src/main/java/org/sonar/ldap/ApacheDS.java
  55. 20
    0
      sonar-testing-ldap/src/main/java/org/sonar/ldap/package-info.java
  56. 38
    0
      sonar-testing-ldap/src/test/java/org/sonar/ldap/ApacheDSTest.java
  57. 5
    0
      sonar-testing-ldap/src/test/resources/change.ldif
  58. 2
    0
      sonar-testing-ldap/src/test/resources/delete.ldif
  59. 9
    0
      sonar-testing-ldap/src/test/resources/init.ldif
  60. 41
    0
      sonar-testing-ldap/src/test/resources/logback-test.xml

+ 37
- 0
.cirrus.yml View File

@@ -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

+ 22
- 0
server/sonar-auth-ldap/build.gradle View File

@@ -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")

}

+ 57
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/CallbackHandlerImpl.java View File

@@ -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");
}
}
}
}

+ 82
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/ContextHelper.java View File

@@ -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);
}
}

}

+ 61
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/Krb5LoginConfiguration.java View File

@@ -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
}
}

+ 130
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAuthenticator.java View File

@@ -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;
}

}

+ 165
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapAutodiscovery.java View File

@@ -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();
}
}

}

+ 247
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapContextFactory.java View File

@@ -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 +
"}";
}

}

+ 32
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapException.java View File

@@ -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);
}

}

+ 152
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupMapping.java View File

@@ -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() +
"}";
}

}

+ 144
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapGroupsProvider.java View File

@@ -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;
}

}

+ 34
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapModule.java View File

@@ -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);
}

}

+ 82
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapRealm.java View File

@@ -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;
}

}

+ 191
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSearch.java View File

@@ -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";
}
}

}

+ 195
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapSettingsManager.java View File

@@ -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);
}
}
}

+ 135
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUserMapping.java View File

@@ -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() +
"}";
}

}

+ 125
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/LdapUsersProvider.java View File

@@ -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;
}

}

+ 23
- 0
server/sonar-auth-ldap/src/main/java/org/sonar/auth/ldap/package-info.java View File

@@ -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;

+ 48
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/CallbackHandlerImplTest.java View File

@@ -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)});
}

}

+ 53
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/ContextHelperTest.java View File

@@ -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);
}

}

+ 84
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/KerberosTest.java View File

@@ -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}))");
}

}

+ 129
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAuthenticatorTest.java View File

@@ -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();
}

}

+ 100
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutoDiscoveryWarningLogTest.java View File

@@ -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");
}

}

+ 93
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapAutodiscoveryTest.java View File

@@ -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"));
}

}

+ 65
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupMappingTest.java View File

@@ -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"});
}

}

+ 149
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapGroupsProviderTest.java View File

@@ -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");
}

}

+ 38
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapModuleTest.java View File

@@ -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);
}

}

+ 82
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapRealmTest.java View File

@@ -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");
}
}

}

+ 69
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapReferralsTest.java View File

@@ -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();
}
}

+ 118
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSearchTest.java View File

@@ -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;
}

}

+ 95
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsFactory.java View File

@@ -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;
}
}

+ 201
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapSettingsManagerTest.java View File

@@ -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;
}

}

+ 76
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUserMappingTest.java View File

@@ -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");
}

}

+ 77
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/LdapUsersProviderTest.java View File

@@ -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");
}

}

+ 234
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/ApacheDS.java View File

@@ -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;
}

}

+ 68
- 0
server/sonar-auth-ldap/src/test/java/org/sonar/auth/ldap/server/LdapServer.java View File

@@ -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();
}

}

+ 20
- 0
server/sonar-auth-ldap/src/test/resources/conf/krb5.conf View File

@@ -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

+ 23
- 0
server/sonar-auth-ldap/src/test/resources/conf/sasl_mech.properties View File

@@ -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

+ 55
- 0
server/sonar-auth-ldap/src/test/resources/krb.ldif View File

@@ -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

+ 45
- 0
server/sonar-auth-ldap/src/test/resources/logback-test.xml View File

@@ -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>

+ 81
- 0
server/sonar-auth-ldap/src/test/resources/static-groups.example.org.ldif View File

@@ -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

+ 88
- 0
server/sonar-auth-ldap/src/test/resources/users-apacheds.ldif View File

@@ -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

+ 98
- 0
server/sonar-auth-ldap/src/test/resources/users.example.org.ldif View File

@@ -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

+ 98
- 0
server/sonar-auth-ldap/src/test/resources/users.infosupport.com.ldif View File

@@ -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

+ 1
- 1
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java View File

@@ -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();
}

+ 1
- 1
server/sonar-docs/src/pages/instance-administration/delegated-auth.md View File

@@ -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 ...

+ 8
- 7
server/sonar-server-common/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java View File

@@ -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)));
}
}


+ 17
- 4
server/sonar-server-common/src/test/java/org/sonar/server/plugins/ServerExtensionInstallerTest.java View File

@@ -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);

+ 1
- 0
server/sonar-webserver/build.gradle View File

@@ -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')

+ 2
- 0
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -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

+ 2
- 0
settings.gradle View File

@@ -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'


+ 0
- 1
sonar-application/build.gradle View File

@@ -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"

+ 15
- 0
sonar-testing-ldap/build.gradle View File

@@ -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'
}

+ 240
- 0
sonar-testing-ldap/src/main/java/org/sonar/ldap/ApacheDS.java View File

@@ -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;
}

}

+ 20
- 0
sonar-testing-ldap/src/main/java/org/sonar/ldap/package-info.java View File

@@ -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;

+ 38
- 0
sonar-testing-ldap/src/test/java/org/sonar/ldap/ApacheDSTest.java View File

@@ -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();
}

}

+ 5
- 0
sonar-testing-ldap/src/test/resources/change.ldif View File

@@ -0,0 +1,5 @@
dn: cn=Evgeny Mandrikov,dc=example,dc=org
changetype: modify
replace: userpassword
userpassword: 54321
-

+ 2
- 0
sonar-testing-ldap/src/test/resources/delete.ldif View File

@@ -0,0 +1,2 @@
dn: cn=Evgeny Mandrikov,dc=example,dc=org
changetype: delete

+ 9
- 0
sonar-testing-ldap/src/test/resources/init.ldif View File

@@ -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

+ 41
- 0
sonar-testing-ldap/src/test/resources/logback-test.xml View File

@@ -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>

Loading…
Cancel
Save