blob: c11d04589546e730297f45d4294f610fc634a518 [file] [log] [blame]
// 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.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.Nullable;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
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 supportAnonymous;
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;
private final Timer0 loginLatencyTimer;
private final Timer0 userSearchLatencyTimer;
private final Timer0 groupSearchLatencyTimer;
private final Timer0 groupExpansionLatencyTimer;
@Inject
Helper(
@GerritServerConfig Config config,
@Named(LdapModule.PARENT_GROUPS_CACHE) Cache<String, ImmutableSet<String>> parentGroups,
MetricMaker metricMaker) {
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.supportAnonymous = config.getBoolean("ldap", "supportAnonymous", true);
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);
this.loginLatencyTimer =
metricMaker.newTimer(
"ldap/login_latency",
new Description("Latency of logins").setCumulative().setUnit(Units.NANOSECONDS));
this.userSearchLatencyTimer =
metricMaker.newTimer(
"ldap/user_search_latency",
new Description("Latency for searching the user account")
.setCumulative()
.setUnit(Units.NANOSECONDS));
this.groupSearchLatencyTimer =
metricMaker.newTimer(
"ldap/group_search_latency",
new Description("Latency for querying the groups membership of an account")
.setCumulative()
.setUnit(Units.NANOSECONDS));
this.groupExpansionLatencyTimer =
metricMaker.newTimer(
"ldap/group_expansion_latency",
new Description("Latency for expanding nested groups")
.setCumulative()
.setUnit(Units.NANOSECONDS));
}
Timer0 getGroupSearchLatencyTimer() {
return groupSearchLatencyTimer;
}
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);
}
if (!supportAnonymous && username != null) {
env.put(Context.SECURITY_PRINCIPAL, username);
env.put(Context.SECURITY_CREDENTIALS, password);
}
LdapContext ctx = createContext(env);
if (supportAnonymous && username != null) {
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, username);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
ctx.reconnect(null);
}
return ctx;
}
@Nullable
private DirContext kerberosOpen(Properties env)
throws IOException, LoginException, NamingException {
LoginContext ctx = new LoginContext("KerberosLogin");
try (Timer0.Context ignored = loginLatencyTimer.start()) {
ctx.login();
}
Subject subject = ctx.getSubject();
try {
return Subject.doAs(
subject, (PrivilegedExceptionAction<DirContext>) () -> 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 (Timer0.Context ignored = loginLatencyTimer.start()) {
env.put(Context.REFERRAL, referral);
if (!supportAnonymous) {
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, dn);
env.put(Context.SECURITY_CREDENTIALS, password);
}
LdapContext ctx = createContext(env);
if (supportAnonymous) {
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
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, userSearchLatencyTimer);
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, groupSearchLatencyTimer)) {
try (Timer0.Context ignored = groupExpansionLatencyTimer.start()) {
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(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.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;
}
}
}
}