Line data Source code
1 : // Copyright (C) 2009 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.auth.ldap;
16 :
17 : import com.google.common.base.Throwables;
18 : import com.google.common.cache.Cache;
19 : import com.google.common.collect.ImmutableSet;
20 : import com.google.common.flogger.FluentLogger;
21 : import com.google.gerrit.common.Nullable;
22 : import com.google.gerrit.common.data.ParameterizedString;
23 : import com.google.gerrit.entities.AccountGroup;
24 : import com.google.gerrit.metrics.Description;
25 : import com.google.gerrit.metrics.Description.Units;
26 : import com.google.gerrit.metrics.MetricMaker;
27 : import com.google.gerrit.metrics.Timer0;
28 : import com.google.gerrit.server.account.AccountException;
29 : import com.google.gerrit.server.account.AuthenticationFailedException;
30 : import com.google.gerrit.server.auth.NoSuchUserException;
31 : import com.google.gerrit.server.config.ConfigUtil;
32 : import com.google.gerrit.server.config.GerritServerConfig;
33 : import com.google.gerrit.util.ssl.BlindHostnameVerifier;
34 : import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
35 : import com.google.inject.Inject;
36 : import com.google.inject.Singleton;
37 : import com.google.inject.name.Named;
38 : import java.io.IOException;
39 : import java.security.PrivilegedActionException;
40 : import java.security.PrivilegedExceptionAction;
41 : import java.util.ArrayList;
42 : import java.util.Collections;
43 : import java.util.HashMap;
44 : import java.util.HashSet;
45 : import java.util.List;
46 : import java.util.Properties;
47 : import java.util.Set;
48 : import java.util.concurrent.TimeUnit;
49 : import javax.naming.CompositeName;
50 : import javax.naming.Context;
51 : import javax.naming.Name;
52 : import javax.naming.NamingEnumeration;
53 : import javax.naming.NamingException;
54 : import javax.naming.PartialResultException;
55 : import javax.naming.directory.Attribute;
56 : import javax.naming.directory.DirContext;
57 : import javax.naming.ldap.InitialLdapContext;
58 : import javax.naming.ldap.LdapContext;
59 : import javax.naming.ldap.StartTlsRequest;
60 : import javax.naming.ldap.StartTlsResponse;
61 : import javax.net.ssl.SSLSocketFactory;
62 : import javax.security.auth.Subject;
63 : import javax.security.auth.login.LoginContext;
64 : import javax.security.auth.login.LoginException;
65 : import org.eclipse.jgit.lib.Config;
66 :
67 : @Singleton
68 : class Helper {
69 2 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
70 :
71 : static final String LDAP_UUID = "ldap:";
72 2 : static final String STARTTLS_PROPERTY = Helper.class.getName() + ".startTls";
73 :
74 : private final Cache<String, ImmutableSet<String>> parentGroups;
75 : private final Config config;
76 : private final String server;
77 : private final String username;
78 : private final String password;
79 : private final String referral;
80 : private final boolean startTls;
81 : private final boolean supportAnonymous;
82 : private final boolean sslVerify;
83 : private final String authentication;
84 : private volatile LdapSchema ldapSchema;
85 : private final String readTimeoutMillis;
86 : private final String connectTimeoutMillis;
87 : private final boolean useConnectionPooling;
88 : private final boolean groupsVisibleToAll;
89 : private final Timer0 loginLatencyTimer;
90 : private final Timer0 userSearchLatencyTimer;
91 : private final Timer0 groupSearchLatencyTimer;
92 : private final Timer0 groupExpansionLatencyTimer;
93 :
94 : @Inject
95 : Helper(
96 : @GerritServerConfig Config config,
97 : @Named(LdapModule.PARENT_GROUPS_CACHE) Cache<String, ImmutableSet<String>> parentGroups,
98 2 : MetricMaker metricMaker) {
99 2 : this.config = config;
100 2 : this.server = LdapRealm.optional(config, "server");
101 2 : this.username = LdapRealm.optional(config, "username");
102 2 : this.password = LdapRealm.optional(config, "password", "");
103 2 : this.referral = LdapRealm.optional(config, "referral", "ignore");
104 2 : this.startTls = config.getBoolean("ldap", "startTls", false);
105 2 : this.supportAnonymous = config.getBoolean("ldap", "supportAnonymous", true);
106 2 : this.sslVerify = config.getBoolean("ldap", "sslverify", true);
107 2 : this.groupsVisibleToAll = config.getBoolean("ldap", "groupsVisibleToAll", false);
108 2 : this.authentication = LdapRealm.optional(config, "authentication", "simple");
109 2 : String readTimeout = LdapRealm.optional(config, "readTimeout");
110 2 : if (readTimeout != null) {
111 0 : readTimeoutMillis =
112 0 : Long.toString(ConfigUtil.getTimeUnit(readTimeout, 0, TimeUnit.MILLISECONDS));
113 : } else {
114 2 : readTimeoutMillis = null;
115 : }
116 2 : String connectTimeout = LdapRealm.optional(config, "connectTimeout");
117 2 : if (connectTimeout != null) {
118 0 : connectTimeoutMillis =
119 0 : Long.toString(ConfigUtil.getTimeUnit(connectTimeout, 0, TimeUnit.MILLISECONDS));
120 : } else {
121 2 : connectTimeoutMillis = null;
122 : }
123 2 : this.parentGroups = parentGroups;
124 2 : this.useConnectionPooling = LdapRealm.optional(config, "useConnectionPooling", false);
125 :
126 2 : this.loginLatencyTimer =
127 2 : metricMaker.newTimer(
128 : "ldap/login_latency",
129 2 : new Description("Latency of logins").setCumulative().setUnit(Units.NANOSECONDS));
130 2 : this.userSearchLatencyTimer =
131 2 : metricMaker.newTimer(
132 : "ldap/user_search_latency",
133 : new Description("Latency for searching the user account")
134 2 : .setCumulative()
135 2 : .setUnit(Units.NANOSECONDS));
136 2 : this.groupSearchLatencyTimer =
137 2 : metricMaker.newTimer(
138 : "ldap/group_search_latency",
139 : new Description("Latency for querying the groups membership of an account")
140 2 : .setCumulative()
141 2 : .setUnit(Units.NANOSECONDS));
142 2 : this.groupExpansionLatencyTimer =
143 2 : metricMaker.newTimer(
144 : "ldap/group_expansion_latency",
145 : new Description("Latency for expanding nested groups")
146 2 : .setCumulative()
147 2 : .setUnit(Units.NANOSECONDS));
148 2 : }
149 :
150 : Timer0 getGroupSearchLatencyTimer() {
151 0 : return groupSearchLatencyTimer;
152 : }
153 :
154 : private Properties createContextProperties() {
155 0 : final Properties env = new Properties();
156 0 : env.put(Context.INITIAL_CONTEXT_FACTORY, LdapRealm.LDAP);
157 0 : env.put(Context.PROVIDER_URL, server);
158 0 : if (server.startsWith("ldaps:") && !sslVerify) {
159 0 : Class<? extends SSLSocketFactory> factory = BlindSSLSocketFactory.class;
160 0 : env.put("java.naming.ldap.factory.socket", factory.getName());
161 : }
162 0 : if (readTimeoutMillis != null) {
163 0 : env.put("com.sun.jndi.ldap.read.timeout", readTimeoutMillis);
164 : }
165 0 : if (connectTimeoutMillis != null) {
166 0 : env.put("com.sun.jndi.ldap.connect.timeout", connectTimeoutMillis);
167 : }
168 0 : if (useConnectionPooling) {
169 0 : env.put("com.sun.jndi.ldap.connect.pool", "true");
170 : }
171 0 : return env;
172 : }
173 :
174 : private LdapContext createContext(Properties env) throws IOException, NamingException {
175 0 : LdapContext ctx = new InitialLdapContext(env, null);
176 0 : if (startTls) {
177 0 : StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
178 0 : SSLSocketFactory sslfactory = null;
179 0 : if (!sslVerify) {
180 0 : sslfactory = (SSLSocketFactory) BlindSSLSocketFactory.getDefault();
181 0 : tls.setHostnameVerifier(BlindHostnameVerifier.getInstance());
182 : }
183 0 : tls.negotiate(sslfactory);
184 0 : ctx.addToEnvironment(STARTTLS_PROPERTY, tls);
185 : }
186 0 : return ctx;
187 : }
188 :
189 : void close(DirContext ctx) {
190 : try {
191 0 : StartTlsResponse tls = (StartTlsResponse) ctx.removeFromEnvironment(STARTTLS_PROPERTY);
192 0 : if (tls != null) {
193 0 : tls.close();
194 : }
195 0 : } catch (IOException | NamingException e) {
196 0 : logger.atWarning().withCause(e).log("Cannot close LDAP startTls handle");
197 0 : }
198 : try {
199 0 : ctx.close();
200 0 : } catch (NamingException e) {
201 0 : logger.atWarning().withCause(e).log("Cannot close LDAP handle");
202 0 : }
203 0 : }
204 :
205 : DirContext open() throws IOException, NamingException, LoginException {
206 0 : final Properties env = createContextProperties();
207 0 : env.put(Context.SECURITY_AUTHENTICATION, authentication);
208 0 : env.put(Context.REFERRAL, referral);
209 0 : if ("GSSAPI".equals(authentication)) {
210 0 : return kerberosOpen(env);
211 : }
212 :
213 0 : if (!supportAnonymous && username != null) {
214 0 : env.put(Context.SECURITY_PRINCIPAL, username);
215 0 : env.put(Context.SECURITY_CREDENTIALS, password);
216 : }
217 :
218 0 : LdapContext ctx = createContext(env);
219 :
220 0 : if (supportAnonymous && username != null) {
221 0 : ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, username);
222 0 : ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
223 0 : ctx.reconnect(null);
224 : }
225 0 : return ctx;
226 : }
227 :
228 : @Nullable
229 : private DirContext kerberosOpen(Properties env)
230 : throws IOException, LoginException, NamingException {
231 0 : LoginContext ctx = new LoginContext("KerberosLogin");
232 0 : try (Timer0.Context ignored = loginLatencyTimer.start()) {
233 0 : ctx.login();
234 : }
235 0 : Subject subject = ctx.getSubject();
236 : try {
237 0 : return Subject.doAs(
238 0 : subject, (PrivilegedExceptionAction<DirContext>) () -> createContext(env));
239 0 : } catch (PrivilegedActionException e) {
240 0 : Throwables.throwIfInstanceOf(e.getException(), IOException.class);
241 0 : Throwables.throwIfInstanceOf(e.getException(), NamingException.class);
242 0 : Throwables.throwIfInstanceOf(e.getException(), RuntimeException.class);
243 0 : logger.atWarning().withCause(e.getException()).log("Internal error");
244 0 : return null;
245 : } finally {
246 0 : ctx.logout();
247 : }
248 : }
249 :
250 : DirContext authenticate(String dn, String password) throws AccountException {
251 0 : final Properties env = createContextProperties();
252 0 : try (Timer0.Context ignored = loginLatencyTimer.start()) {
253 0 : env.put(Context.REFERRAL, referral);
254 :
255 0 : if (!supportAnonymous) {
256 0 : env.put(Context.SECURITY_AUTHENTICATION, "simple");
257 0 : env.put(Context.SECURITY_PRINCIPAL, dn);
258 0 : env.put(Context.SECURITY_CREDENTIALS, password);
259 : }
260 :
261 0 : LdapContext ctx = createContext(env);
262 :
263 0 : if (supportAnonymous) {
264 0 : ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
265 0 : ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
266 0 : ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
267 0 : ctx.reconnect(null);
268 : }
269 :
270 0 : return ctx;
271 0 : } catch (IOException | NamingException e) {
272 0 : throw new AuthenticationFailedException("Incorrect username or password", e);
273 : }
274 : }
275 :
276 : LdapSchema getSchema(DirContext ctx) {
277 0 : if (ldapSchema == null) {
278 0 : synchronized (this) {
279 0 : if (ldapSchema == null) {
280 0 : ldapSchema = new LdapSchema(ctx);
281 : }
282 0 : }
283 : }
284 0 : return ldapSchema;
285 : }
286 :
287 : LdapQuery.Result findAccount(
288 : Helper.LdapSchema schema, DirContext ctx, String username, boolean fetchMemberOf)
289 : throws NamingException, AccountException {
290 0 : final HashMap<String, String> params = new HashMap<>();
291 0 : params.put(LdapRealm.USERNAME, username);
292 :
293 : List<LdapQuery> accountQueryList;
294 0 : if (fetchMemberOf && schema.type.accountMemberField() != null) {
295 0 : accountQueryList = schema.accountWithMemberOfQueryList;
296 : } else {
297 0 : accountQueryList = schema.accountQueryList;
298 : }
299 :
300 0 : for (LdapQuery accountQuery : accountQueryList) {
301 0 : List<LdapQuery.Result> res = accountQuery.query(ctx, params, userSearchLatencyTimer);
302 0 : if (res.size() == 1) {
303 0 : return res.get(0);
304 0 : } else if (res.size() > 1) {
305 0 : throw new AccountException("Duplicate users: " + username);
306 : }
307 0 : }
308 0 : throw new NoSuchUserException(username);
309 : }
310 :
311 : Set<AccountGroup.UUID> queryForGroups(
312 : final DirContext ctx, String username, LdapQuery.Result account) throws NamingException {
313 0 : final LdapSchema schema = getSchema(ctx);
314 0 : final Set<String> groupDNs = new HashSet<>();
315 :
316 0 : if (!schema.groupMemberQueryList.isEmpty()) {
317 0 : final HashMap<String, String> params = new HashMap<>();
318 :
319 0 : if (account == null) {
320 : try {
321 0 : account = findAccount(schema, ctx, username, false);
322 0 : } catch (AccountException e) {
323 0 : return Collections.emptySet();
324 0 : }
325 : }
326 0 : for (String name : schema.groupMemberQueryList.get(0).getParameters()) {
327 0 : params.put(name, account.get(name));
328 0 : }
329 :
330 0 : params.put(LdapRealm.USERNAME, username);
331 :
332 0 : for (LdapQuery groupMemberQuery : schema.groupMemberQueryList) {
333 0 : for (LdapQuery.Result r : groupMemberQuery.query(ctx, params, groupSearchLatencyTimer)) {
334 0 : try (Timer0.Context ignored = groupExpansionLatencyTimer.start()) {
335 0 : recursivelyExpandGroups(groupDNs, schema, ctx, r.getDN());
336 : }
337 0 : }
338 0 : }
339 : }
340 :
341 0 : if (schema.accountMemberField != null) {
342 0 : if (account == null || account.getAll(schema.accountMemberField) == null) {
343 : try {
344 0 : account = findAccount(schema, ctx, username, true);
345 0 : } catch (AccountException e) {
346 0 : return Collections.emptySet();
347 0 : }
348 : }
349 :
350 0 : final Attribute groupAtt = account.getAll(schema.accountMemberField);
351 0 : if (groupAtt != null) {
352 0 : final NamingEnumeration<?> groups = groupAtt.getAll();
353 : try {
354 0 : while (groups.hasMore()) {
355 0 : final String nextDN = (String) groups.next();
356 0 : recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
357 0 : }
358 0 : } catch (PartialResultException e) {
359 : // Ignored
360 0 : }
361 : }
362 : }
363 :
364 0 : final Set<AccountGroup.UUID> actual = new HashSet<>();
365 0 : for (String dn : groupDNs) {
366 0 : actual.add(AccountGroup.uuid(LDAP_UUID + dn));
367 0 : }
368 :
369 0 : if (actual.isEmpty()) {
370 0 : return Collections.emptySet();
371 : }
372 0 : return ImmutableSet.copyOf(actual);
373 : }
374 :
375 : private void recursivelyExpandGroups(
376 : final Set<String> groupDNs,
377 : final LdapSchema schema,
378 : final DirContext ctx,
379 : final String groupDN) {
380 0 : if (groupDNs.add(groupDN)
381 : && schema.accountMemberField != null
382 : && schema.accountMemberExpandGroups) {
383 0 : ImmutableSet<String> cachedParentsDNs = parentGroups.getIfPresent(groupDN);
384 0 : if (cachedParentsDNs == null) {
385 : // Recursively identify the groups it is a member of.
386 0 : ImmutableSet.Builder<String> dns = ImmutableSet.builder();
387 : try {
388 0 : final Name compositeGroupName = new CompositeName().add(groupDN);
389 0 : final Attribute in =
390 0 : ctx.getAttributes(compositeGroupName, schema.accountMemberFieldArray)
391 0 : .get(schema.accountMemberField);
392 0 : if (in != null) {
393 0 : final NamingEnumeration<?> groups = in.getAll();
394 : try {
395 0 : while (groups.hasMore()) {
396 0 : dns.add((String) groups.next());
397 : }
398 0 : } catch (PartialResultException e) {
399 : // Ignored
400 0 : }
401 : }
402 0 : } catch (NamingException e) {
403 0 : logger.atWarning().withCause(e).log("Could not find group %s", groupDN);
404 0 : }
405 0 : cachedParentsDNs = dns.build();
406 0 : parentGroups.put(groupDN, cachedParentsDNs);
407 : }
408 0 : for (String dn : cachedParentsDNs) {
409 0 : recursivelyExpandGroups(groupDNs, schema, ctx, dn);
410 0 : }
411 : }
412 0 : }
413 :
414 : public boolean groupsVisibleToAll() {
415 0 : return this.groupsVisibleToAll;
416 : }
417 :
418 : class LdapSchema {
419 : final LdapType type;
420 :
421 : final ParameterizedString accountFullName;
422 : final ParameterizedString accountEmailAddress;
423 : final ParameterizedString accountSshUserName;
424 : final String accountMemberField;
425 : final boolean accountMemberExpandGroups;
426 : final String[] accountMemberFieldArray;
427 : final List<LdapQuery> accountQueryList;
428 : final List<LdapQuery> accountWithMemberOfQueryList;
429 :
430 : final List<String> groupBases;
431 : final SearchScope groupScope;
432 : final ParameterizedString groupPattern;
433 : final ParameterizedString groupName;
434 : final List<LdapQuery> groupMemberQueryList;
435 :
436 0 : LdapSchema(DirContext ctx) {
437 0 : type = discoverLdapType(ctx);
438 0 : groupMemberQueryList = new ArrayList<>();
439 0 : accountQueryList = new ArrayList<>();
440 0 : accountWithMemberOfQueryList = new ArrayList<>();
441 :
442 0 : final Set<String> accountAtts = new HashSet<>();
443 :
444 : // Group query
445 : //
446 :
447 0 : groupBases = LdapRealm.optionalList(config, "groupBase");
448 0 : groupScope = LdapRealm.scope(config, "groupScope");
449 0 : groupPattern = LdapRealm.paramString(config, "groupPattern", type.groupPattern());
450 0 : groupName = LdapRealm.paramString(config, "groupName", type.groupName());
451 0 : final String groupMemberPattern =
452 0 : LdapRealm.optdef(config, "groupMemberPattern", type.groupMemberPattern());
453 :
454 0 : for (String groupBase : groupBases) {
455 0 : if (groupMemberPattern != null) {
456 0 : final LdapQuery groupMemberQuery =
457 : new LdapQuery(
458 : groupBase,
459 : groupScope,
460 : new ParameterizedString(groupMemberPattern),
461 0 : Collections.emptySet());
462 0 : if (groupMemberQuery.getParameters().isEmpty()) {
463 0 : throw new IllegalArgumentException("No variables in ldap.groupMemberPattern");
464 : }
465 :
466 0 : accountAtts.addAll(groupMemberQuery.getParameters());
467 :
468 0 : groupMemberQueryList.add(groupMemberQuery);
469 : }
470 0 : }
471 :
472 : // Account query
473 : //
474 0 : accountFullName = LdapRealm.paramString(config, "accountFullName", type.accountFullName());
475 0 : if (accountFullName != null) {
476 0 : accountAtts.addAll(accountFullName.getParameterNames());
477 : }
478 0 : accountEmailAddress =
479 0 : LdapRealm.paramString(config, "accountEmailAddress", type.accountEmailAddress());
480 0 : if (accountEmailAddress != null) {
481 0 : accountAtts.addAll(accountEmailAddress.getParameterNames());
482 : }
483 0 : accountSshUserName =
484 0 : LdapRealm.paramString(config, "accountSshUserName", type.accountSshUserName());
485 0 : if (accountSshUserName != null) {
486 0 : accountAtts.addAll(accountSshUserName.getParameterNames());
487 : }
488 0 : accountMemberField =
489 0 : LdapRealm.optdef(config, "accountMemberField", type.accountMemberField());
490 0 : if (accountMemberField != null) {
491 0 : accountMemberFieldArray = new String[] {accountMemberField};
492 : } else {
493 0 : accountMemberFieldArray = null;
494 : }
495 0 : accountMemberExpandGroups =
496 0 : LdapRealm.optional(config, "accountMemberExpandGroups", type.accountMemberExpandGroups());
497 :
498 0 : final SearchScope accountScope = LdapRealm.scope(config, "accountScope");
499 0 : final String accountPattern =
500 0 : LdapRealm.reqdef(config, "accountPattern", type.accountPattern());
501 :
502 : Set<String> accountWithMemberOfAtts;
503 0 : if (accountMemberField != null) {
504 0 : accountWithMemberOfAtts = new HashSet<>(accountAtts);
505 0 : accountWithMemberOfAtts.add(accountMemberField);
506 : } else {
507 0 : accountWithMemberOfAtts = null;
508 : }
509 0 : for (String accountBase : LdapRealm.requiredList(config, "accountBase")) {
510 0 : LdapQuery accountQuery =
511 : new LdapQuery(
512 : accountBase, accountScope, new ParameterizedString(accountPattern), accountAtts);
513 0 : if (accountQuery.getParameters().isEmpty()) {
514 0 : throw new IllegalArgumentException("No variables in ldap.accountPattern");
515 : }
516 0 : accountQueryList.add(accountQuery);
517 :
518 0 : if (accountWithMemberOfAtts != null) {
519 0 : LdapQuery accountWithMemberOfQuery =
520 : new LdapQuery(
521 : accountBase,
522 : accountScope,
523 : new ParameterizedString(accountPattern),
524 : accountWithMemberOfAtts);
525 0 : accountWithMemberOfQueryList.add(accountWithMemberOfQuery);
526 : }
527 0 : }
528 0 : }
529 :
530 : LdapType discoverLdapType(DirContext ctx) {
531 : try {
532 0 : return LdapType.guessType(ctx);
533 0 : } catch (NamingException e) {
534 0 : logger.atWarning().withCause(e).log(
535 : "Cannot discover type of LDAP server at %s,"
536 : + " assuming the server is RFC 2307 compliant.",
537 : server);
538 0 : return LdapType.RFC_2307;
539 : }
540 : }
541 : }
542 : }
|