@@ -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 |
@@ -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") | |||
} |
@@ -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"); | |||
} | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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 + | |||
"}"; | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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() + | |||
"}"; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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"; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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() + | |||
"}"; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; |
@@ -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)}); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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}))"); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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")); | |||
} | |||
} |
@@ -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"}); | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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"); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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> |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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(); | |||
} |
@@ -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 ... |
@@ -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))); | |||
} | |||
} | |||
@@ -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); |
@@ -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') |
@@ -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 |
@@ -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' | |||
@@ -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" |
@@ -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' | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; |
@@ -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(); | |||
} | |||
} |
@@ -0,0 +1,5 @@ | |||
dn: cn=Evgeny Mandrikov,dc=example,dc=org | |||
changetype: modify | |||
replace: userpassword | |||
userpassword: 54321 | |||
- |
@@ -0,0 +1,2 @@ | |||
dn: cn=Evgeny Mandrikov,dc=example,dc=org | |||
changetype: delete |
@@ -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 |
@@ -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> |