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 static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
18 :
19 : import com.google.common.base.Strings;
20 : import com.google.common.cache.CacheLoader;
21 : import com.google.common.cache.LoadingCache;
22 : import com.google.common.flogger.FluentLogger;
23 : import com.google.gerrit.common.Nullable;
24 : import com.google.gerrit.common.data.ParameterizedString;
25 : import com.google.gerrit.entities.Account;
26 : import com.google.gerrit.entities.AccountGroup;
27 : import com.google.gerrit.entities.GroupReference;
28 : import com.google.gerrit.extensions.client.AccountFieldName;
29 : import com.google.gerrit.extensions.client.AuthType;
30 : import com.google.gerrit.server.account.AbstractRealm;
31 : import com.google.gerrit.server.account.AccountException;
32 : import com.google.gerrit.server.account.AuthRequest;
33 : import com.google.gerrit.server.account.EmailExpander;
34 : import com.google.gerrit.server.account.GroupBackends;
35 : import com.google.gerrit.server.account.externalids.ExternalId;
36 : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
37 : import com.google.gerrit.server.account.externalids.ExternalIds;
38 : import com.google.gerrit.server.auth.AuthenticationUnavailableException;
39 : import com.google.gerrit.server.auth.NoSuchUserException;
40 : import com.google.gerrit.server.config.AuthConfig;
41 : import com.google.gerrit.server.config.GerritServerConfig;
42 : import com.google.gerrit.server.logging.Metadata;
43 : import com.google.gerrit.server.logging.TraceContext;
44 : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
45 : import com.google.inject.Inject;
46 : import com.google.inject.Singleton;
47 : import com.google.inject.name.Named;
48 : import java.io.IOException;
49 : import java.util.Arrays;
50 : import java.util.Collection;
51 : import java.util.HashMap;
52 : import java.util.HashSet;
53 : import java.util.List;
54 : import java.util.Locale;
55 : import java.util.Map;
56 : import java.util.Optional;
57 : import java.util.Set;
58 : import java.util.concurrent.ExecutionException;
59 : import javax.naming.CompositeName;
60 : import javax.naming.Name;
61 : import javax.naming.NamingException;
62 : import javax.naming.directory.DirContext;
63 : import javax.security.auth.login.LoginException;
64 : import org.eclipse.jgit.lib.Config;
65 :
66 : @Singleton
67 : class LdapRealm extends AbstractRealm {
68 2 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
69 :
70 : static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
71 : static final String USERNAME = "username";
72 :
73 : private final Helper helper;
74 : private final AuthConfig authConfig;
75 : private final EmailExpander emailExpander;
76 : private final LoadingCache<String, Optional<Account.Id>> usernameCache;
77 : private final Set<AccountFieldName> readOnlyAccountFields;
78 : private final boolean fetchMemberOfEagerly;
79 : private final String mandatoryGroup;
80 : private final LdapGroupBackend groupBackend;
81 :
82 : private final Config config;
83 :
84 : private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
85 :
86 : @Inject
87 : LdapRealm(
88 : Helper helper,
89 : AuthConfig authConfig,
90 : EmailExpander emailExpander,
91 : LdapGroupBackend groupBackend,
92 : @Named(LdapModule.GROUP_CACHE) LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
93 : @Named(LdapModule.USERNAME_CACHE) LoadingCache<String, Optional<Account.Id>> usernameCache,
94 2 : @GerritServerConfig Config config) {
95 2 : this.helper = helper;
96 2 : this.authConfig = authConfig;
97 2 : this.emailExpander = emailExpander;
98 2 : this.groupBackend = groupBackend;
99 2 : this.usernameCache = usernameCache;
100 2 : this.membershipCache = membershipCache;
101 2 : this.config = config;
102 :
103 2 : this.readOnlyAccountFields = new HashSet<>();
104 :
105 2 : if (optdef(config, "accountFullName", "DEFAULT") != null) {
106 2 : readOnlyAccountFields.add(AccountFieldName.FULL_NAME);
107 : }
108 2 : if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
109 2 : readOnlyAccountFields.add(AccountFieldName.USER_NAME);
110 : }
111 2 : if (!authConfig.isAllowRegisterNewEmail()) {
112 0 : readOnlyAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL);
113 : }
114 :
115 2 : fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
116 2 : mandatoryGroup = optional(config, "mandatoryGroup");
117 2 : }
118 :
119 : static SearchScope scope(Config c, String setting) {
120 0 : return c.getEnum("ldap", null, setting, SearchScope.SUBTREE);
121 : }
122 :
123 : static String optional(Config config, String name) {
124 2 : return config.getString("ldap", null, name);
125 : }
126 :
127 : static int optional(Config config, String name, int defaultValue) {
128 0 : return config.getInt("ldap", name, defaultValue);
129 : }
130 :
131 : static String optional(Config config, String name, String defaultValue) {
132 2 : final String v = optional(config, name);
133 2 : if (Strings.isNullOrEmpty(v)) {
134 2 : return defaultValue;
135 : }
136 0 : return v;
137 : }
138 :
139 : static boolean optional(Config config, String name, boolean defaultValue) {
140 2 : return config.getBoolean("ldap", name, defaultValue);
141 : }
142 :
143 : static String required(Config config, String name) {
144 0 : final String v = optional(config, name);
145 0 : if (v == null || "".equals(v)) {
146 0 : throw new IllegalArgumentException("No ldap." + name + " configured");
147 : }
148 0 : return v;
149 : }
150 :
151 : static List<String> optionalList(Config config, String name) {
152 0 : String[] s = config.getStringList("ldap", null, name);
153 0 : return Arrays.asList(s);
154 : }
155 :
156 : static List<String> requiredList(Config config, String name) {
157 0 : List<String> vlist = optionalList(config, name);
158 :
159 0 : if (vlist.isEmpty()) {
160 0 : throw new IllegalArgumentException("No ldap " + name + " configured");
161 : }
162 :
163 0 : return vlist;
164 : }
165 :
166 : @Nullable
167 : static String optdef(Config c, String n, String d) {
168 2 : final String[] v = c.getStringList("ldap", null, n);
169 2 : if (v == null || v.length == 0) {
170 2 : return d;
171 :
172 0 : } else if (v[0] == null || "".equals(v[0])) {
173 0 : return null;
174 :
175 : } else {
176 0 : checkBackendCompliance(n, v[0], Strings.isNullOrEmpty(d));
177 0 : return v[0];
178 : }
179 : }
180 :
181 : static String reqdef(Config c, String n, String d) {
182 0 : final String v = optdef(c, n, d);
183 0 : if (v == null) {
184 0 : throw new IllegalArgumentException("No ldap." + n + " configured");
185 : }
186 0 : return v;
187 : }
188 :
189 : @Nullable
190 : static ParameterizedString paramString(Config c, String n, String d) {
191 0 : String expression = optdef(c, n, d);
192 0 : if (expression == null) {
193 0 : return null;
194 0 : } else if (expression.contains("${")) {
195 0 : return new ParameterizedString(expression);
196 : } else {
197 0 : return new ParameterizedString("${" + expression + "}");
198 : }
199 : }
200 :
201 : private static void checkBackendCompliance(
202 : String configOption, String suppliedValue, boolean disabledByBackend) {
203 0 : if (disabledByBackend && !Strings.isNullOrEmpty(suppliedValue)) {
204 0 : String msg = String.format("LDAP backend doesn't support: ldap.%s", configOption);
205 0 : logger.atSevere().log("%s", msg);
206 0 : throw new IllegalArgumentException(msg);
207 : }
208 0 : }
209 :
210 : @Override
211 : public boolean allowsEdit(AccountFieldName field) {
212 1 : return !readOnlyAccountFields.contains(field);
213 : }
214 :
215 : @Nullable
216 : static String apply(ParameterizedString p, LdapQuery.Result m) throws NamingException {
217 0 : if (p == null) {
218 0 : return null;
219 : }
220 :
221 0 : final Map<String, String> values = new HashMap<>();
222 0 : for (String name : m.attributes()) {
223 0 : values.put(name, m.get(name));
224 0 : }
225 :
226 0 : String r = p.replace(values).trim();
227 0 : return r.isEmpty() ? null : r;
228 : }
229 :
230 : @Override
231 : public AuthRequest authenticate(AuthRequest who) throws AccountException {
232 0 : if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) {
233 0 : who.setLocalUser(who.getLocalUser().toLowerCase(Locale.US));
234 : }
235 :
236 0 : final String username = who.getLocalUser();
237 : try {
238 : final DirContext ctx;
239 0 : if (authConfig.getAuthType() == AuthType.LDAP_BIND) {
240 0 : ctx = helper.authenticate(username, who.getPassword());
241 : } else {
242 0 : ctx = helper.open();
243 : }
244 : try {
245 0 : final Helper.LdapSchema schema = helper.getSchema(ctx);
246 : LdapQuery.Result m;
247 0 : who.setAuthProvidesAccountActiveStatus(true);
248 0 : m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly);
249 0 : who.setActive(true);
250 :
251 0 : if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) {
252 : // We found the user account, but we need to verify
253 : // the password matches it before we can continue.
254 : //
255 0 : helper.close(helper.authenticate(m.getDN(), who.getPassword()));
256 : }
257 :
258 0 : who.setDisplayName(apply(schema.accountFullName, m));
259 0 : who.setUserName(apply(schema.accountSshUserName, m));
260 :
261 0 : if (schema.accountEmailAddress != null) {
262 0 : who.setEmailAddress(apply(schema.accountEmailAddress, m));
263 :
264 0 : } else if (emailExpander.canExpand(username)) {
265 : // If LDAP cannot give us a valid email address for this user
266 : // try expanding it through the older email expander code which
267 : // assumes a user name within a domain.
268 : //
269 0 : who.setEmailAddress(emailExpander.expand(username));
270 : }
271 :
272 : // Fill the cache with the user's current groups. We've already
273 : // spent the cost to open the LDAP connection, we might as well
274 : // do one more call to get their group membership. Since we are
275 : // in the middle of authenticating the user, its likely we will
276 : // need to know what access rights they have soon.
277 : //
278 0 : if (fetchMemberOfEagerly || mandatoryGroup != null) {
279 0 : Set<AccountGroup.UUID> groups = helper.queryForGroups(ctx, username, m);
280 0 : if (mandatoryGroup != null) {
281 0 : GroupReference mandatoryGroupRef =
282 0 : GroupBackends.findExactSuggestion(groupBackend, mandatoryGroup);
283 0 : if (mandatoryGroupRef == null) {
284 0 : throw new AccountException("Could not identify mandatory group: " + mandatoryGroup);
285 : }
286 0 : if (!groups.contains(mandatoryGroupRef.getUUID())) {
287 0 : throw new AccountException(
288 0 : "Not member of mandatory LDAP group: " + mandatoryGroupRef.getName());
289 : }
290 : }
291 : // Regardless if we enabled fetchMemberOfEagerly, we already have the
292 : // groups and it would be a waste not to cache them.
293 0 : membershipCache.put(username, groups);
294 : }
295 0 : return who;
296 : } finally {
297 0 : helper.close(ctx);
298 : }
299 0 : } catch (IOException | NamingException e) {
300 0 : logger.atSevere().withCause(e).log("Cannot query LDAP to authenticate user");
301 0 : throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
302 0 : } catch (LoginException e) {
303 0 : logger.atSevere().withCause(e).log("Cannot authenticate server via JAAS");
304 0 : throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
305 : }
306 : }
307 :
308 : @Override
309 : public void onCreateAccount(AuthRequest who, Account account) {
310 0 : usernameCache.put(who.getLocalUser(), Optional.of(account.id()));
311 0 : }
312 :
313 : @Nullable
314 : @Override
315 : public Account.Id lookup(String accountName) {
316 0 : if (Strings.isNullOrEmpty(accountName)) {
317 0 : return null;
318 : }
319 : try {
320 0 : Optional<Account.Id> id = usernameCache.get(accountName);
321 0 : return id != null ? id.orElse(null) : null;
322 0 : } catch (ExecutionException e) {
323 0 : logger.atWarning().withCause(e).log("Cannot lookup account %s in LDAP", accountName);
324 0 : return null;
325 : }
326 : }
327 :
328 : @Override
329 : public boolean isActive(String username)
330 : throws LoginException, NamingException, AccountException, IOException {
331 0 : final DirContext ctx = helper.open();
332 : try {
333 0 : Helper.LdapSchema schema = helper.getSchema(ctx);
334 0 : helper.findAccount(schema, ctx, username, false);
335 0 : return true;
336 0 : } catch (NoSuchUserException e) {
337 0 : return false;
338 : } finally {
339 0 : helper.close(ctx);
340 : }
341 : }
342 :
343 : @Override
344 : public boolean accountBelongsToRealm(Collection<ExternalId> externalIds) {
345 2 : for (ExternalId id : externalIds) {
346 2 : if (id.isScheme(SCHEME_GERRIT)) {
347 2 : return true;
348 : }
349 2 : }
350 2 : return false;
351 : }
352 :
353 : static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
354 : private final ExternalIds externalIds;
355 : private final ExternalIdKeyFactory externalIdKeyFactory;
356 :
357 : @Inject
358 2 : UserLoader(ExternalIds externalIds, ExternalIdKeyFactory externalIdKeyFactory) {
359 2 : this.externalIds = externalIds;
360 2 : this.externalIdKeyFactory = externalIdKeyFactory;
361 2 : }
362 :
363 : @Override
364 : public Optional<Account.Id> load(String username) throws Exception {
365 0 : try (TraceTimer timer =
366 0 : TraceContext.newTimer(
367 0 : "Loading account for username", Metadata.builder().username(username).build())) {
368 0 : return externalIds
369 0 : .get(externalIdKeyFactory.create(SCHEME_GERRIT, username))
370 0 : .map(ExternalId::accountId);
371 : }
372 : }
373 : }
374 :
375 : static class MemberLoader extends CacheLoader<String, Set<AccountGroup.UUID>> {
376 : private final Helper helper;
377 :
378 : @Inject
379 2 : MemberLoader(Helper helper) {
380 2 : this.helper = helper;
381 2 : }
382 :
383 : @Override
384 : public Set<AccountGroup.UUID> load(String username) throws Exception {
385 0 : try (TraceTimer timer =
386 0 : TraceContext.newTimer(
387 : "Loading group for member with username",
388 0 : Metadata.builder().username(username).build())) {
389 0 : final DirContext ctx = helper.open();
390 : try {
391 0 : return helper.queryForGroups(ctx, username, null);
392 : } finally {
393 0 : helper.close(ctx);
394 : }
395 : }
396 : }
397 : }
398 :
399 : static class ExistenceLoader extends CacheLoader<String, Boolean> {
400 : private final Helper helper;
401 :
402 : @Inject
403 2 : ExistenceLoader(Helper helper) {
404 2 : this.helper = helper;
405 2 : }
406 :
407 : @Override
408 : public Boolean load(String groupDn) throws Exception {
409 0 : try (TraceTimer timer =
410 0 : TraceContext.newTimer(
411 0 : "Loading groupDn", Metadata.builder().authDomainName(groupDn).build())) {
412 0 : final DirContext ctx = helper.open();
413 : try {
414 0 : Name compositeGroupName = new CompositeName().add(groupDn);
415 : try {
416 0 : ctx.getAttributes(compositeGroupName);
417 0 : return true;
418 0 : } catch (NamingException e) {
419 0 : return false;
420 : }
421 : } finally {
422 0 : helper.close(ctx);
423 : }
424 0 : }
425 : }
426 : }
427 : }
|