diff options
Diffstat (limited to 'java/com/google/gerrit/server/auth/ldap/Helper.java')
-rw-r--r-- | java/com/google/gerrit/server/auth/ldap/Helper.java | 486 |
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; + } + } + } +} |