summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/auth/ldap/Helper.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/auth/ldap/Helper.java')
-rw-r--r--java/com/google/gerrit/server/auth/ldap/Helper.java486
1 files changed, 486 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/auth/ldap/Helper.java b/java/com/google/gerrit/server/auth/ldap/Helper.java
new file mode 100644
index 0000000000..a53a8c2caf
--- /dev/null
+++ b/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -0,0 +1,486 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth.ldap;
+
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AuthenticationFailedException;
+import com.google.gerrit.server.auth.NoSuchUserException;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.util.ssl.BlindHostnameVerifier;
+import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.naming.CompositeName;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.PartialResultException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.DirContext;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.StartTlsRequest;
+import javax.naming.ldap.StartTlsResponse;
+import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+class Helper {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ static final String LDAP_UUID = "ldap:";
+ static final String STARTTLS_PROPERTY = Helper.class.getName() + ".startTls";
+
+ private final Cache<String, ImmutableSet<String>> parentGroups;
+ private final Config config;
+ private final String server;
+ private final String username;
+ private final String password;
+ private final String referral;
+ private final boolean startTls;
+ private final boolean sslVerify;
+ private final String authentication;
+ private volatile LdapSchema ldapSchema;
+ private final String readTimeoutMillis;
+ private final String connectTimeoutMillis;
+ private final boolean useConnectionPooling;
+ private final boolean groupsVisibleToAll;
+
+ @Inject
+ Helper(
+ @GerritServerConfig Config config,
+ @Named(LdapModule.PARENT_GROUPS_CACHE) Cache<String, ImmutableSet<String>> parentGroups) {
+ this.config = config;
+ this.server = LdapRealm.optional(config, "server");
+ this.username = LdapRealm.optional(config, "username");
+ this.password = LdapRealm.optional(config, "password", "");
+ this.referral = LdapRealm.optional(config, "referral", "ignore");
+ this.startTls = config.getBoolean("ldap", "startTls", false);
+ this.sslVerify = config.getBoolean("ldap", "sslverify", true);
+ this.groupsVisibleToAll = config.getBoolean("ldap", "groupsVisibleToAll", false);
+ this.authentication = LdapRealm.optional(config, "authentication", "simple");
+ String readTimeout = LdapRealm.optional(config, "readTimeout");
+ if (readTimeout != null) {
+ readTimeoutMillis =
+ Long.toString(ConfigUtil.getTimeUnit(readTimeout, 0, TimeUnit.MILLISECONDS));
+ } else {
+ readTimeoutMillis = null;
+ }
+ String connectTimeout = LdapRealm.optional(config, "connectTimeout");
+ if (connectTimeout != null) {
+ connectTimeoutMillis =
+ Long.toString(ConfigUtil.getTimeUnit(connectTimeout, 0, TimeUnit.MILLISECONDS));
+ } else {
+ connectTimeoutMillis = null;
+ }
+ this.parentGroups = parentGroups;
+ this.useConnectionPooling = LdapRealm.optional(config, "useConnectionPooling", false);
+ }
+
+ private Properties createContextProperties() {
+ final Properties env = new Properties();
+ env.put(Context.INITIAL_CONTEXT_FACTORY, LdapRealm.LDAP);
+ env.put(Context.PROVIDER_URL, server);
+ if (server.startsWith("ldaps:") && !sslVerify) {
+ Class<? extends SSLSocketFactory> factory = BlindSSLSocketFactory.class;
+ env.put("java.naming.ldap.factory.socket", factory.getName());
+ }
+ if (readTimeoutMillis != null) {
+ env.put("com.sun.jndi.ldap.read.timeout", readTimeoutMillis);
+ }
+ if (connectTimeoutMillis != null) {
+ env.put("com.sun.jndi.ldap.connect.timeout", connectTimeoutMillis);
+ }
+ if (useConnectionPooling) {
+ env.put("com.sun.jndi.ldap.connect.pool", "true");
+ }
+ return env;
+ }
+
+ private LdapContext createContext(Properties env) throws IOException, NamingException {
+ LdapContext ctx = new InitialLdapContext(env, null);
+ if (startTls) {
+ StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
+ SSLSocketFactory sslfactory = null;
+ if (!sslVerify) {
+ sslfactory = (SSLSocketFactory) BlindSSLSocketFactory.getDefault();
+ tls.setHostnameVerifier(BlindHostnameVerifier.getInstance());
+ }
+ tls.negotiate(sslfactory);
+ ctx.addToEnvironment(STARTTLS_PROPERTY, tls);
+ }
+ return ctx;
+ }
+
+ void close(DirContext ctx) {
+ try {
+ StartTlsResponse tls = (StartTlsResponse) ctx.removeFromEnvironment(STARTTLS_PROPERTY);
+ if (tls != null) {
+ tls.close();
+ }
+ } catch (IOException | NamingException e) {
+ logger.atWarning().withCause(e).log("Cannot close LDAP startTls handle");
+ }
+ try {
+ ctx.close();
+ } catch (NamingException e) {
+ logger.atWarning().withCause(e).log("Cannot close LDAP handle");
+ }
+ }
+
+ DirContext open() throws IOException, NamingException, LoginException {
+ final Properties env = createContextProperties();
+ env.put(Context.SECURITY_AUTHENTICATION, authentication);
+ env.put(Context.REFERRAL, referral);
+ if ("GSSAPI".equals(authentication)) {
+ return kerberosOpen(env);
+ }
+ LdapContext ctx = createContext(env);
+ if (username != null) {
+ ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, username);
+ ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
+ ctx.reconnect(null);
+ }
+ return ctx;
+ }
+
+ private DirContext kerberosOpen(Properties env)
+ throws IOException, LoginException, NamingException {
+ LoginContext ctx = new LoginContext("KerberosLogin");
+ ctx.login();
+ Subject subject = ctx.getSubject();
+ try {
+ return Subject.doAs(
+ subject,
+ new PrivilegedExceptionAction<DirContext>() {
+ @Override
+ public DirContext run() throws IOException, NamingException {
+ return createContext(env);
+ }
+ });
+ } catch (PrivilegedActionException e) {
+ Throwables.throwIfInstanceOf(e.getException(), IOException.class);
+ Throwables.throwIfInstanceOf(e.getException(), NamingException.class);
+ Throwables.throwIfInstanceOf(e.getException(), RuntimeException.class);
+ logger.atWarning().withCause(e.getException()).log("Internal error");
+ return null;
+ } finally {
+ ctx.logout();
+ }
+ }
+
+ DirContext authenticate(String dn, String password) throws AccountException {
+ final Properties env = createContextProperties();
+ try {
+ LdapContext ctx = createContext(env);
+ ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
+ ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
+ ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
+ ctx.addToEnvironment(Context.REFERRAL, referral);
+ ctx.reconnect(null);
+ return ctx;
+ } catch (IOException | NamingException e) {
+ throw new AuthenticationFailedException("Incorrect username or password", e);
+ }
+ }
+
+ LdapSchema getSchema(DirContext ctx) {
+ if (ldapSchema == null) {
+ synchronized (this) {
+ if (ldapSchema == null) {
+ ldapSchema = new LdapSchema(ctx);
+ }
+ }
+ }
+ return ldapSchema;
+ }
+
+ LdapQuery.Result findAccount(
+ Helper.LdapSchema schema, DirContext ctx, String username, boolean fetchMemberOf)
+ throws NamingException, AccountException {
+ final HashMap<String, String> params = new HashMap<>();
+ params.put(LdapRealm.USERNAME, username);
+
+ List<LdapQuery> accountQueryList;
+ if (fetchMemberOf && schema.type.accountMemberField() != null) {
+ accountQueryList = schema.accountWithMemberOfQueryList;
+ } else {
+ accountQueryList = schema.accountQueryList;
+ }
+
+ for (LdapQuery accountQuery : accountQueryList) {
+ List<LdapQuery.Result> res = accountQuery.query(ctx, params);
+ if (res.size() == 1) {
+ return res.get(0);
+ } else if (res.size() > 1) {
+ throw new AccountException("Duplicate users: " + username);
+ }
+ }
+ throw new NoSuchUserException(username);
+ }
+
+ Set<AccountGroup.UUID> queryForGroups(
+ final DirContext ctx, String username, LdapQuery.Result account) throws NamingException {
+ final LdapSchema schema = getSchema(ctx);
+ final Set<String> groupDNs = new HashSet<>();
+
+ if (!schema.groupMemberQueryList.isEmpty()) {
+ final HashMap<String, String> params = new HashMap<>();
+
+ if (account == null) {
+ try {
+ account = findAccount(schema, ctx, username, false);
+ } catch (AccountException e) {
+ return Collections.emptySet();
+ }
+ }
+ for (String name : schema.groupMemberQueryList.get(0).getParameters()) {
+ params.put(name, account.get(name));
+ }
+
+ params.put(LdapRealm.USERNAME, username);
+
+ for (LdapQuery groupMemberQuery : schema.groupMemberQueryList) {
+ for (LdapQuery.Result r : groupMemberQuery.query(ctx, params)) {
+ recursivelyExpandGroups(groupDNs, schema, ctx, r.getDN());
+ }
+ }
+ }
+
+ if (schema.accountMemberField != null) {
+ if (account == null || account.getAll(schema.accountMemberField) == null) {
+ try {
+ account = findAccount(schema, ctx, username, true);
+ } catch (AccountException e) {
+ return Collections.emptySet();
+ }
+ }
+
+ final Attribute groupAtt = account.getAll(schema.accountMemberField);
+ if (groupAtt != null) {
+ final NamingEnumeration<?> groups = groupAtt.getAll();
+ try {
+ while (groups.hasMore()) {
+ final String nextDN = (String) groups.next();
+ recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
+ }
+ } catch (PartialResultException e) {
+ // Ignored
+ }
+ }
+ }
+
+ final Set<AccountGroup.UUID> actual = new HashSet<>();
+ for (String dn : groupDNs) {
+ actual.add(new AccountGroup.UUID(LDAP_UUID + dn));
+ }
+
+ if (actual.isEmpty()) {
+ return Collections.emptySet();
+ }
+ return ImmutableSet.copyOf(actual);
+ }
+
+ private void recursivelyExpandGroups(
+ final Set<String> groupDNs,
+ final LdapSchema schema,
+ final DirContext ctx,
+ final String groupDN) {
+ if (groupDNs.add(groupDN)
+ && schema.accountMemberField != null
+ && schema.accountMemberExpandGroups) {
+ ImmutableSet<String> cachedParentsDNs = parentGroups.getIfPresent(groupDN);
+ if (cachedParentsDNs == null) {
+ // Recursively identify the groups it is a member of.
+ ImmutableSet.Builder<String> dns = ImmutableSet.builder();
+ try {
+ final Name compositeGroupName = new CompositeName().add(groupDN);
+ final Attribute in =
+ ctx.getAttributes(compositeGroupName, schema.accountMemberFieldArray)
+ .get(schema.accountMemberField);
+ if (in != null) {
+ final NamingEnumeration<?> groups = in.getAll();
+ try {
+ while (groups.hasMore()) {
+ dns.add((String) groups.next());
+ }
+ } catch (PartialResultException e) {
+ // Ignored
+ }
+ }
+ } catch (NamingException e) {
+ logger.atWarning().withCause(e).log("Could not find group %s", groupDN);
+ }
+ cachedParentsDNs = dns.build();
+ parentGroups.put(groupDN, cachedParentsDNs);
+ }
+ for (String dn : cachedParentsDNs) {
+ recursivelyExpandGroups(groupDNs, schema, ctx, dn);
+ }
+ }
+ }
+
+ public boolean groupsVisibleToAll() {
+ return this.groupsVisibleToAll;
+ }
+
+ class LdapSchema {
+ final LdapType type;
+
+ final ParameterizedString accountFullName;
+ final ParameterizedString accountEmailAddress;
+ final ParameterizedString accountSshUserName;
+ final String accountMemberField;
+ final boolean accountMemberExpandGroups;
+ final String[] accountMemberFieldArray;
+ final List<LdapQuery> accountQueryList;
+ final List<LdapQuery> accountWithMemberOfQueryList;
+
+ final List<String> groupBases;
+ final SearchScope groupScope;
+ final ParameterizedString groupPattern;
+ final ParameterizedString groupName;
+ final List<LdapQuery> groupMemberQueryList;
+
+ LdapSchema(DirContext ctx) {
+ type = discoverLdapType(ctx);
+ groupMemberQueryList = new ArrayList<>();
+ accountQueryList = new ArrayList<>();
+ accountWithMemberOfQueryList = new ArrayList<>();
+
+ final Set<String> accountAtts = new HashSet<>();
+
+ // Group query
+ //
+
+ groupBases = LdapRealm.optionalList(config, "groupBase");
+ groupScope = LdapRealm.scope(config, "groupScope");
+ groupPattern = LdapRealm.paramString(config, "groupPattern", type.groupPattern());
+ groupName = LdapRealm.paramString(config, "groupName", type.groupName());
+ final String groupMemberPattern =
+ LdapRealm.optdef(config, "groupMemberPattern", type.groupMemberPattern());
+
+ for (String groupBase : groupBases) {
+ if (groupMemberPattern != null) {
+ final LdapQuery groupMemberQuery =
+ new LdapQuery(
+ groupBase,
+ groupScope,
+ new ParameterizedString(groupMemberPattern),
+ Collections.<String>emptySet());
+ if (groupMemberQuery.getParameters().isEmpty()) {
+ throw new IllegalArgumentException("No variables in ldap.groupMemberPattern");
+ }
+
+ accountAtts.addAll(groupMemberQuery.getParameters());
+
+ groupMemberQueryList.add(groupMemberQuery);
+ }
+ }
+
+ // Account query
+ //
+ accountFullName = LdapRealm.paramString(config, "accountFullName", type.accountFullName());
+ if (accountFullName != null) {
+ accountAtts.addAll(accountFullName.getParameterNames());
+ }
+ accountEmailAddress =
+ LdapRealm.paramString(config, "accountEmailAddress", type.accountEmailAddress());
+ if (accountEmailAddress != null) {
+ accountAtts.addAll(accountEmailAddress.getParameterNames());
+ }
+ accountSshUserName =
+ LdapRealm.paramString(config, "accountSshUserName", type.accountSshUserName());
+ if (accountSshUserName != null) {
+ accountAtts.addAll(accountSshUserName.getParameterNames());
+ }
+ accountMemberField =
+ LdapRealm.optdef(config, "accountMemberField", type.accountMemberField());
+ if (accountMemberField != null) {
+ accountMemberFieldArray = new String[] {accountMemberField};
+ } else {
+ accountMemberFieldArray = null;
+ }
+ accountMemberExpandGroups =
+ LdapRealm.optional(config, "accountMemberExpandGroups", type.accountMemberExpandGroups());
+
+ final SearchScope accountScope = LdapRealm.scope(config, "accountScope");
+ final String accountPattern =
+ LdapRealm.reqdef(config, "accountPattern", type.accountPattern());
+
+ Set<String> accountWithMemberOfAtts;
+ if (accountMemberField != null) {
+ accountWithMemberOfAtts = new HashSet<>(accountAtts);
+ accountWithMemberOfAtts.add(accountMemberField);
+ } else {
+ accountWithMemberOfAtts = null;
+ }
+ for (String accountBase : LdapRealm.requiredList(config, "accountBase")) {
+ LdapQuery accountQuery =
+ new LdapQuery(
+ accountBase, accountScope, new ParameterizedString(accountPattern), accountAtts);
+ if (accountQuery.getParameters().isEmpty()) {
+ throw new IllegalArgumentException("No variables in ldap.accountPattern");
+ }
+ accountQueryList.add(accountQuery);
+
+ if (accountWithMemberOfAtts != null) {
+ LdapQuery accountWithMemberOfQuery =
+ new LdapQuery(
+ accountBase,
+ accountScope,
+ new ParameterizedString(accountPattern),
+ accountWithMemberOfAtts);
+ accountWithMemberOfQueryList.add(accountWithMemberOfQuery);
+ }
+ }
+ }
+
+ LdapType discoverLdapType(DirContext ctx) {
+ try {
+ return LdapType.guessType(ctx);
+ } catch (NamingException e) {
+ logger.atWarning().withCause(e).log(
+ "Cannot discover type of LDAP server at %s,"
+ + " assuming the server is RFC 2307 compliant.",
+ server);
+ return LdapType.RFC_2307;
+ }
+ }
+ }
+}