path: "**/test-results/**/*.xml" | path: "**/test-results/**/*.xml" | ||||
format: junit | 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: | promote_task: | ||||
depends_on: | depends_on: | ||||
- build | - build | ||||
- validate | - validate | ||||
- qa | - qa | ||||
- qa_saml | - qa_saml | ||||
- qa_ldap | |||||
only_if: $CIRRUS_BRANCH !=~ "dogfood/.*" && $CIRRUS_BRANCH != "public_master" && $CIRRUS_BRANCH != "branch-nightly-build" | only_if: $CIRRUS_BRANCH !=~ "dogfood/.*" && $CIRRUS_BRANCH != "public_master" && $CIRRUS_BRANCH != "branch-nightly-build" | ||||
gke_container: | gke_container: | ||||
dockerfile: private/docker/Dockerfile-build | dockerfile: private/docker/Dockerfile-build |
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") | |||||
} |
/* | |||||
* 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"); | |||||
} | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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 | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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(); | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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 + | |||||
"}"; | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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() + | |||||
"}"; | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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"; | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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() + | |||||
"}"; | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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; |
/* | |||||
* 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)}); | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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}))"); | |||||
} | |||||
} |
/* | |||||
* 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(); | |||||
} | |||||
} |
/* | |||||
* 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"); | |||||
} | |||||
} |
/* | |||||
* 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")); | |||||
} | |||||
} |
/* | |||||
* 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"}); | |||||
} | |||||
} |
/* | |||||
* 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"); | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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"); | |||||
} | |||||
} | |||||
} |
/* | |||||
* 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(); | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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"); | |||||
} | |||||
} |
/* | |||||
* 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"); | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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(); | |||||
} | |||||
} |
[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 |
# | |||||
# 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 |
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 |
<?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> |
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 |
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 |
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 |
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 |
setSettings(true); | setSettings(true); | ||||
assertThat(underTest.getKey()).isEqualTo("saml"); | assertThat(underTest.getKey()).isEqualTo("saml"); | ||||
assertThat(underTest.getName()).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.getDisplay().getBackgroundColor()).isEqualTo("#444444"); | ||||
assertThat(underTest.allowsUsersToSignUp()).isTrue(); | assertThat(underTest.allowsUsersToSignUp()).isTrue(); | ||||
} | } |
![](/images/check.svg) = successfully tested | ![](/images/check.svg) = successfully tested | ||||
### Setup | ### 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: | 2. Restart the SonarQube server and check the log file for: | ||||
``` | ``` | ||||
INFO org.sonar.INFO Security realm: LDAP ... | INFO org.sonar.INFO Security realm: LDAP ... |
import com.google.common.collect.ListMultimap; | import com.google.common.collect.ListMultimap; | ||||
import java.lang.annotation.Annotation; | import java.lang.annotation.Annotation; | ||||
import java.util.Collection; | import java.util.Collection; | ||||
import java.util.List; | |||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.TreeSet; | |||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
import org.sonar.api.ExtensionProvider; | import org.sonar.api.ExtensionProvider; | ||||
import org.sonar.api.Plugin; | import org.sonar.api.Plugin; | ||||
import org.sonar.core.platform.PluginInfo; | import org.sonar.core.platform.PluginInfo; | ||||
import org.sonar.core.platform.PluginRepository; | import org.sonar.core.platform.PluginRepository; | ||||
import static java.lang.String.format; | |||||
import static java.util.Objects.requireNonNull; | import static java.util.Objects.requireNonNull; | ||||
import static org.sonar.core.extension.ExtensionProviderSupport.isExtensionProvider; | import static org.sonar.core.extension.ExtensionProviderSupport.isExtensionProvider; | ||||
*/ | */ | ||||
public abstract class ServerExtensionInstaller { | 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 SonarRuntime sonarRuntime; | ||||
private final PluginRepository pluginRepository; | private final PluginRepository pluginRepository; | ||||
} | } | ||||
} catch (Throwable e) { | } catch (Throwable e) { | ||||
// catch Throwable because we want to catch Error too (IncompatibleClassChangeError, ...) | // 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()) { | for (Map.Entry<PluginInfo, Object> entry : installedExtensionsByPlugin.entries()) { | ||||
} | } | ||||
} catch (Throwable e) { | } catch (Throwable e) { | ||||
// catch Throwable because we want to catch Error too (IncompatibleClassChangeError, ...) | // 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() { | private void failWhenNoMoreCompatiblePlugins() { | ||||
List<String> noMoreCompatiblePluginNames = pluginRepository.getPluginInfos() | |||||
Set<String> noMoreCompatiblePluginNames = pluginRepository.getPluginInfos() | |||||
.stream() | .stream() | ||||
.filter(pluginInfo -> NO_MORE_COMPATIBLE_PLUGINS.contains(pluginInfo.getKey())) | .filter(pluginInfo -> NO_MORE_COMPATIBLE_PLUGINS.contains(pluginInfo.getKey())) | ||||
.map(PluginInfo::getName) | .map(PluginInfo::getName) | ||||
.collect(Collectors.toList()); | |||||
.collect(Collectors.toCollection(TreeSet::new)); | |||||
if (!noMoreCompatiblePluginNames.isEmpty()) { | 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))); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@Test | @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(); | ComponentContainer componentContainer = new ComponentContainer(); | ||||
expectedException.expect(MessageException.class); | 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); | underTest.installExtensions(componentContainer); | ||||
} | } | ||||
underTest.installExtensions(componentContainer); | 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) { | private static PluginInfo newPlugin(String key, String name) { | ||||
PluginInfo plugin = mock(PluginInfo.class); | PluginInfo plugin = mock(PluginInfo.class); | ||||
when(plugin.getKey()).thenReturn(key); | when(plugin.getKey()).thenReturn(key); |
compile project(':sonar-core') | compile project(':sonar-core') | ||||
compile project(':server:sonar-auth-github') | compile project(':server:sonar-auth-github') | ||||
compile project(':server:sonar-auth-gitlab') | compile project(':server:sonar-auth-gitlab') | ||||
compile project(':server:sonar-auth-ldap') | |||||
compile project(':server:sonar-auth-saml') | compile project(':server:sonar-auth-saml') | ||||
compile project(':server:sonar-ce-task-projectanalysis') | compile project(':server:sonar-ce-task-projectanalysis') | ||||
compile project(':server:sonar-process') | compile project(':server:sonar-process') |
import org.sonar.api.server.rule.RulesDefinitionXmlLoader; | import org.sonar.api.server.rule.RulesDefinitionXmlLoader; | ||||
import org.sonar.auth.github.GitHubModule; | import org.sonar.auth.github.GitHubModule; | ||||
import org.sonar.auth.gitlab.GitLabModule; | import org.sonar.auth.gitlab.GitLabModule; | ||||
import org.sonar.auth.ldap.LdapModule; | |||||
import org.sonar.auth.saml.SamlModule; | import org.sonar.auth.saml.SamlModule; | ||||
import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationModule; | import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationModule; | ||||
import org.sonar.ce.task.projectanalysis.taskprocessor.ReportTaskProcessor; | import org.sonar.ce.task.projectanalysis.taskprocessor.ReportTaskProcessor; | ||||
AuthenticationWsModule.class, | AuthenticationWsModule.class, | ||||
GitHubModule.class, | GitHubModule.class, | ||||
GitLabModule.class, | GitLabModule.class, | ||||
LdapModule.class, | |||||
SamlModule.class, | SamlModule.class, | ||||
// users | // users |
include 'server:sonar-auth-common' | include 'server:sonar-auth-common' | ||||
include 'server:sonar-auth-github' | include 'server:sonar-auth-github' | ||||
include 'server:sonar-auth-gitlab' | include 'server:sonar-auth-gitlab' | ||||
include 'server:sonar-auth-ldap' | |||||
include 'server:sonar-auth-saml' | include 'server:sonar-auth-saml' | ||||
include 'server:sonar-ce' | include 'server:sonar-ce' | ||||
include 'server:sonar-ce-common' | include 'server:sonar-ce-common' | ||||
include 'sonar-scanner-protocol' | include 'sonar-scanner-protocol' | ||||
include 'sonar-shutdowner' | include 'sonar-shutdowner' | ||||
include 'sonar-testing-harness' | include 'sonar-testing-harness' | ||||
include 'sonar-testing-ldap' | |||||
include 'sonar-ws' | include 'sonar-ws' | ||||
include 'sonar-ws-generator' | include 'sonar-ws-generator' | ||||
bundledPlugin 'org.sonarsource.java:sonar-java-plugin@jar' | bundledPlugin 'org.sonarsource.java:sonar-java-plugin@jar' | ||||
bundledPlugin 'org.sonarsource.jacoco:sonar-jacoco-plugin:1.0.2.475@jar' | bundledPlugin 'org.sonarsource.jacoco:sonar-jacoco-plugin:1.0.2.475@jar' | ||||
bundledPlugin 'org.sonarsource.javascript:sonar-javascript-plugin@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.php:sonar-php-plugin@jar' | ||||
bundledPlugin 'org.sonarsource.python:sonar-python-plugin@jar' | bundledPlugin 'org.sonarsource.python:sonar-python-plugin@jar' | ||||
bundledPlugin "org.sonarsource.slang:sonar-kotlin-plugin@jar" | bundledPlugin "org.sonarsource.slang:sonar-kotlin-plugin@jar" |
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' | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
/* | |||||
* 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; |
/* | |||||
* 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(); | |||||
} | |||||
} |
dn: cn=Evgeny Mandrikov,dc=example,dc=org | |||||
changetype: modify | |||||
replace: userpassword | |||||
userpassword: 54321 | |||||
- |
dn: cn=Evgeny Mandrikov,dc=example,dc=org | |||||
changetype: delete |
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 |
<?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> |