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.server;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 : import static com.google.common.flogger.LazyArgs.lazy;
20 :
21 : import com.google.common.annotations.VisibleForTesting;
22 : import com.google.common.base.Strings;
23 : import com.google.common.collect.ImmutableSet;
24 : import com.google.common.collect.Sets;
25 : import com.google.common.flogger.FluentLogger;
26 : import com.google.gerrit.common.Nullable;
27 : import com.google.gerrit.common.UsedAt;
28 : import com.google.gerrit.entities.Account;
29 : import com.google.gerrit.server.account.AccountCache;
30 : import com.google.gerrit.server.account.AccountState;
31 : import com.google.gerrit.server.account.GroupBackend;
32 : import com.google.gerrit.server.account.GroupMembership;
33 : import com.google.gerrit.server.account.ListGroupMembership;
34 : import com.google.gerrit.server.account.Realm;
35 : import com.google.gerrit.server.account.externalids.ExternalId;
36 : import com.google.gerrit.server.config.AnonymousCowardName;
37 : import com.google.gerrit.server.config.AuthConfig;
38 : import com.google.gerrit.server.config.CanonicalWebUrl;
39 : import com.google.gerrit.server.config.EnablePeerIPInReflogRecord;
40 : import com.google.gerrit.server.group.SystemGroupBackend;
41 : import com.google.inject.Inject;
42 : import com.google.inject.OutOfScopeException;
43 : import com.google.inject.Provider;
44 : import com.google.inject.ProvisionException;
45 : import com.google.inject.Singleton;
46 : import com.google.inject.util.Providers;
47 : import java.net.InetAddress;
48 : import java.net.InetSocketAddress;
49 : import java.net.MalformedURLException;
50 : import java.net.SocketAddress;
51 : import java.net.URL;
52 : import java.time.Instant;
53 : import java.time.ZoneId;
54 : import java.util.Optional;
55 : import java.util.Set;
56 : import org.eclipse.jgit.lib.PersonIdent;
57 : import org.eclipse.jgit.util.SystemReader;
58 :
59 : /** An authenticated user. */
60 : public class IdentifiedUser extends CurrentUser {
61 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
62 :
63 : /** Create an IdentifiedUser, ignoring any per-request state. */
64 : @Singleton
65 : public static class GenericFactory {
66 : private final AuthConfig authConfig;
67 : private final Realm realm;
68 : private final String anonymousCowardName;
69 : private final Provider<String> canonicalUrl;
70 : private final AccountCache accountCache;
71 : private final GroupBackend groupBackend;
72 : private final Boolean enablePeerIPInReflogRecord;
73 :
74 : @Inject
75 : public GenericFactory(
76 : AuthConfig authConfig,
77 : Realm realm,
78 : @AnonymousCowardName String anonymousCowardName,
79 : @CanonicalWebUrl Provider<String> canonicalUrl,
80 : @EnablePeerIPInReflogRecord Boolean enablePeerIPInReflogRecord,
81 : AccountCache accountCache,
82 152 : GroupBackend groupBackend) {
83 152 : this.authConfig = authConfig;
84 152 : this.realm = realm;
85 152 : this.anonymousCowardName = anonymousCowardName;
86 152 : this.canonicalUrl = canonicalUrl;
87 152 : this.accountCache = accountCache;
88 152 : this.groupBackend = groupBackend;
89 152 : this.enablePeerIPInReflogRecord = enablePeerIPInReflogRecord;
90 152 : }
91 :
92 : public IdentifiedUser create(AccountState state) {
93 147 : return new IdentifiedUser(
94 : authConfig,
95 : realm,
96 : anonymousCowardName,
97 : canonicalUrl,
98 : accountCache,
99 : groupBackend,
100 : enablePeerIPInReflogRecord,
101 147 : Providers.of(null),
102 : state,
103 : null);
104 : }
105 :
106 : public IdentifiedUser create(Account.Id id) {
107 151 : return create(null, id);
108 : }
109 :
110 : @VisibleForTesting
111 : @UsedAt(UsedAt.Project.GOOGLE)
112 : public IdentifiedUser forTest(Account.Id id, PropertyMap properties) {
113 0 : return runAs(null, id, null, properties);
114 : }
115 :
116 : public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
117 151 : return runAs(remotePeer, id, null);
118 : }
119 :
120 : public IdentifiedUser runAs(
121 : SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
122 151 : return runAs(remotePeer, id, caller, PropertyMap.EMPTY);
123 : }
124 :
125 : private IdentifiedUser runAs(
126 : SocketAddress remotePeer,
127 : Account.Id id,
128 : @Nullable CurrentUser caller,
129 : PropertyMap properties) {
130 151 : return new IdentifiedUser(
131 : authConfig,
132 : realm,
133 : anonymousCowardName,
134 : canonicalUrl,
135 : accountCache,
136 : groupBackend,
137 : enablePeerIPInReflogRecord,
138 151 : Providers.of(remotePeer),
139 : id,
140 : caller,
141 : properties);
142 : }
143 : }
144 :
145 : /**
146 : * Create an IdentifiedUser, relying on current request state.
147 : *
148 : * <p>Can only be used from within a module that has defined a request scoped {@code @RemotePeer
149 : * SocketAddress} provider.
150 : */
151 : @Singleton
152 : public static class RequestFactory {
153 : private final AuthConfig authConfig;
154 : private final Realm realm;
155 : private final String anonymousCowardName;
156 : private final Provider<String> canonicalUrl;
157 : private final AccountCache accountCache;
158 : private final GroupBackend groupBackend;
159 : private final Boolean enablePeerIPInReflogRecord;
160 : private final Provider<SocketAddress> remotePeerProvider;
161 :
162 : @Inject
163 : RequestFactory(
164 : AuthConfig authConfig,
165 : Realm realm,
166 : @AnonymousCowardName String anonymousCowardName,
167 : @CanonicalWebUrl Provider<String> canonicalUrl,
168 : AccountCache accountCache,
169 : GroupBackend groupBackend,
170 : @EnablePeerIPInReflogRecord Boolean enablePeerIPInReflogRecord,
171 138 : @RemotePeer Provider<SocketAddress> remotePeerProvider) {
172 138 : this.authConfig = authConfig;
173 138 : this.realm = realm;
174 138 : this.anonymousCowardName = anonymousCowardName;
175 138 : this.canonicalUrl = canonicalUrl;
176 138 : this.accountCache = accountCache;
177 138 : this.groupBackend = groupBackend;
178 138 : this.enablePeerIPInReflogRecord = enablePeerIPInReflogRecord;
179 138 : this.remotePeerProvider = remotePeerProvider;
180 138 : }
181 :
182 : public IdentifiedUser create(Account.Id id) {
183 10 : return create(id, PropertyMap.EMPTY);
184 : }
185 :
186 : public <T> IdentifiedUser create(Account.Id id, PropertyMap properties) {
187 11 : return new IdentifiedUser(
188 : authConfig,
189 : realm,
190 : anonymousCowardName,
191 : canonicalUrl,
192 : accountCache,
193 : groupBackend,
194 : enablePeerIPInReflogRecord,
195 : remotePeerProvider,
196 : id,
197 : null,
198 : properties);
199 : }
200 :
201 : public IdentifiedUser runAs(Account.Id id, CurrentUser caller, PropertyMap properties) {
202 37 : return new IdentifiedUser(
203 : authConfig,
204 : realm,
205 : anonymousCowardName,
206 : canonicalUrl,
207 : accountCache,
208 : groupBackend,
209 : enablePeerIPInReflogRecord,
210 : remotePeerProvider,
211 : id,
212 : caller,
213 : properties);
214 : }
215 : }
216 :
217 152 : private static final GroupMembership registeredGroups =
218 : new ListGroupMembership(
219 152 : ImmutableSet.of(SystemGroupBackend.ANONYMOUS_USERS, SystemGroupBackend.REGISTERED_USERS));
220 :
221 : private final Provider<String> canonicalUrl;
222 : private final AccountCache accountCache;
223 : private final AuthConfig authConfig;
224 : private final Realm realm;
225 : private final GroupBackend groupBackend;
226 : private final String anonymousCowardName;
227 : private final Boolean enablePeerIPInReflogRecord;
228 151 : private final Set<String> validEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
229 : private final CurrentUser realUser; // Must be final since cached properties depend on it.
230 :
231 : private final Provider<SocketAddress> remotePeerProvider;
232 : private final Account.Id accountId;
233 :
234 : private AccountState state;
235 : private boolean loadedAllEmails;
236 : private Set<String> invalidEmails;
237 : private GroupMembership effectiveGroups;
238 :
239 : private IdentifiedUser(
240 : AuthConfig authConfig,
241 : Realm realm,
242 : String anonymousCowardName,
243 : Provider<String> canonicalUrl,
244 : AccountCache accountCache,
245 : GroupBackend groupBackend,
246 : Boolean enablePeerIPInReflogRecord,
247 : @Nullable Provider<SocketAddress> remotePeerProvider,
248 : AccountState state,
249 : @Nullable CurrentUser realUser) {
250 147 : this(
251 : authConfig,
252 : realm,
253 : anonymousCowardName,
254 : canonicalUrl,
255 : accountCache,
256 : groupBackend,
257 : enablePeerIPInReflogRecord,
258 : remotePeerProvider,
259 147 : state.account().id(),
260 : realUser,
261 : PropertyMap.EMPTY);
262 147 : this.state = state;
263 147 : }
264 :
265 : private IdentifiedUser(
266 : AuthConfig authConfig,
267 : Realm realm,
268 : String anonymousCowardName,
269 : Provider<String> canonicalUrl,
270 : AccountCache accountCache,
271 : GroupBackend groupBackend,
272 : Boolean enablePeerIPInReflogRecord,
273 : @Nullable Provider<SocketAddress> remotePeerProvider,
274 : Account.Id id,
275 : @Nullable CurrentUser realUser,
276 : PropertyMap properties) {
277 151 : super(properties);
278 151 : this.canonicalUrl = canonicalUrl;
279 151 : this.accountCache = accountCache;
280 151 : this.groupBackend = groupBackend;
281 151 : this.authConfig = authConfig;
282 151 : this.realm = realm;
283 151 : this.anonymousCowardName = anonymousCowardName;
284 151 : this.enablePeerIPInReflogRecord = enablePeerIPInReflogRecord;
285 151 : this.remotePeerProvider = remotePeerProvider;
286 151 : this.accountId = id;
287 151 : this.realUser = realUser != null ? realUser : this;
288 151 : }
289 :
290 : @Override
291 : public CurrentUser getRealUser() {
292 103 : return realUser;
293 : }
294 :
295 : @Override
296 : public boolean isImpersonating() {
297 150 : if (realUser == this) {
298 150 : return false;
299 : }
300 2 : if (realUser.isIdentifiedUser()) {
301 2 : if (realUser.getAccountId().equals(getAccountId())) {
302 : // Impersonating another copy of this user is allowed.
303 0 : return false;
304 : }
305 : }
306 2 : return true;
307 : }
308 :
309 : /**
310 : * Returns the account state of the identified user.
311 : *
312 : * @return the account state of the identified user, an empty account state if the account is
313 : * missing
314 : */
315 : public AccountState state() {
316 151 : if (state == null) {
317 : // TODO(ekempin):
318 : // Ideally we would only create IdentifiedUser instances for existing accounts. To ensure
319 : // this we could load the account state eagerly on the creation of IdentifiedUser and fail is
320 : // the account is missing. In most cases, e.g. when creating an IdentifiedUser for a request
321 : // context, we really want to fail early if the account is missing. However there are some
322 : // usages where an IdentifiedUser may be instantiated for a missing account. We may go
323 : // through all of them and ensure that they never try to create an IdentifiedUser for a
324 : // missing account or make this explicit by adding a createEvenIfMissing method to
325 : // IdentifiedUser.GenericFactory. However since this is a lot of effort we stick with calling
326 : // AccountCache#getEvenIfMissing(Account.Id) for now.
327 : // Alternatively we could be could also return an Optional<AccountState> from the state()
328 : // method and let callers handle the missing account case explicitly. But this would be a lot
329 : // of work too.
330 151 : state = accountCache.getEvenIfMissing(getAccountId());
331 : }
332 151 : return state;
333 : }
334 :
335 : @Override
336 : public IdentifiedUser asIdentifiedUser() {
337 150 : return this;
338 : }
339 :
340 : @Override
341 : public Account.Id getAccountId() {
342 151 : return accountId;
343 : }
344 :
345 : /**
346 : * Returns the user's user name; null if one has not been selected/assigned or if the user name is
347 : * empty.
348 : */
349 : @Override
350 : public Optional<String> getUserName() {
351 151 : return state().userName();
352 : }
353 :
354 : /** Returns unique name of the user for logging, never {@code null} */
355 : @Override
356 : public String getLoggableName() {
357 150 : return getUserName()
358 150 : .orElseGet(() -> firstNonNull(getAccount().preferredEmail(), "a/" + getAccountId().get()));
359 : }
360 :
361 : /**
362 : * Returns the account of the identified user.
363 : *
364 : * @return the account of the identified user, an empty account if the account is missing
365 : */
366 : public Account getAccount() {
367 151 : return state().account();
368 : }
369 :
370 : public boolean hasEmailAddress(String email) {
371 110 : if (validEmails.contains(email)) {
372 101 : return true;
373 108 : } else if (invalidEmails != null && invalidEmails.contains(email)) {
374 28 : return false;
375 108 : } else if (realm.hasEmailAddress(this, email)) {
376 104 : validEmails.add(email);
377 104 : return true;
378 38 : } else if (invalidEmails == null) {
379 38 : invalidEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
380 : }
381 38 : invalidEmails.add(email);
382 38 : return false;
383 : }
384 :
385 : @Override
386 : public ImmutableSet<String> getEmailAddresses() {
387 52 : if (!loadedAllEmails) {
388 52 : validEmails.addAll(realm.getEmailAddresses(this));
389 52 : loadedAllEmails = true;
390 : }
391 52 : return ImmutableSet.copyOf(validEmails);
392 : }
393 :
394 : @Override
395 : public ImmutableSet<ExternalId.Key> getExternalIdKeys() {
396 2 : return state().externalIds().stream().map(ExternalId::key).collect(toImmutableSet());
397 : }
398 :
399 : public String getName() {
400 41 : return getAccount().getName();
401 : }
402 :
403 : public String getNameEmail() {
404 6 : return getAccount().getNameEmail(anonymousCowardName);
405 : }
406 :
407 : @Override
408 : public GroupMembership getEffectiveGroups() {
409 150 : if (effectiveGroups == null) {
410 150 : if (authConfig.isIdentityTrustable(state().externalIds())) {
411 150 : effectiveGroups = groupBackend.membershipsOf(this);
412 150 : logger.atFinest().log(
413 150 : "Known groups of %s: %s", getLoggableName(), lazy(effectiveGroups::getKnownGroups));
414 : } else {
415 3 : effectiveGroups = registeredGroups;
416 3 : logger.atFinest().log(
417 : "%s has a non-trusted identity, falling back to %s as known groups",
418 3 : getLoggableName(), lazy(registeredGroups::getKnownGroups));
419 : }
420 : }
421 150 : return effectiveGroups;
422 : }
423 :
424 : @Override
425 : public Object getCacheKey() {
426 145 : return getAccountId();
427 : }
428 :
429 : public PersonIdent newRefLogIdent() {
430 100 : return newRefLogIdent(Instant.now(), ZoneId.systemDefault());
431 : }
432 :
433 : public PersonIdent newRefLogIdent(Instant when, ZoneId zoneId) {
434 113 : final Account ua = getAccount();
435 :
436 113 : String name = ua.fullName();
437 113 : if (name == null || name.isEmpty()) {
438 15 : name = ua.preferredEmail();
439 : }
440 113 : if (name == null || name.isEmpty()) {
441 13 : name = anonymousCowardName;
442 : }
443 :
444 : String user;
445 113 : if (enablePeerIPInReflogRecord) {
446 1 : user = constructMailAddress(ua, guessHost());
447 : } else {
448 : user =
449 113 : Strings.isNullOrEmpty(ua.preferredEmail())
450 13 : ? constructMailAddress(ua, "unknown")
451 113 : : ua.preferredEmail();
452 : }
453 :
454 113 : return new PersonIdent(name, user, when, zoneId);
455 : }
456 :
457 : private String constructMailAddress(Account ua, String host) {
458 14 : return getUserName().orElse("") + "|account-" + ua.id().toString() + "@" + host;
459 : }
460 :
461 : public PersonIdent newCommitterIdent(PersonIdent ident) {
462 151 : return newCommitterIdent(ident.getWhenAsInstant(), ident.getZoneId());
463 : }
464 :
465 : public PersonIdent newCommitterIdent(Instant when, ZoneId zoneId) {
466 151 : final Account ua = getAccount();
467 151 : String name = ua.fullName();
468 151 : String email = ua.preferredEmail();
469 :
470 151 : if (email == null || email.isEmpty()) {
471 : // No preferred email is configured. Use a generic identity so we
472 : // don't leak an address the user may have given us, but doesn't
473 : // necessarily want to publish through Git records.
474 : //
475 17 : String user = getUserName().orElseGet(() -> "account-" + ua.id().toString());
476 :
477 : String host;
478 17 : if (canonicalUrl.get() != null) {
479 : try {
480 17 : host = new URL(canonicalUrl.get()).getHost();
481 0 : } catch (MalformedURLException e) {
482 0 : host = SystemReader.getInstance().getHostname();
483 17 : }
484 : } else {
485 0 : host = SystemReader.getInstance().getHostname();
486 : }
487 :
488 17 : email = user + "@" + host;
489 : }
490 :
491 151 : if (name == null || name.isEmpty()) {
492 18 : final int at = email.indexOf('@');
493 18 : if (0 < at) {
494 18 : name = email.substring(0, at);
495 : } else {
496 0 : name = anonymousCowardName;
497 : }
498 : }
499 :
500 151 : return new PersonIdent(name, email, when, zoneId);
501 : }
502 :
503 : @Override
504 : public String toString() {
505 5 : return "IdentifiedUser[account " + getAccountId() + "]";
506 : }
507 :
508 : /** Check if user is the IdentifiedUser */
509 : @Override
510 : public boolean isIdentifiedUser() {
511 150 : return true;
512 : }
513 :
514 : /**
515 : * Returns a materialized copy of the user with all dependencies.
516 : *
517 : * <p>Invoke all providers and factories of dependent objects and store the references to a copy
518 : * of the current identified user.
519 : *
520 : * @return copy of the identified user
521 : */
522 : public IdentifiedUser materializedCopy() {
523 : Provider<SocketAddress> remotePeer;
524 : try {
525 96 : remotePeer = Providers.of(remotePeerProvider.get());
526 0 : } catch (OutOfScopeException | ProvisionException e) {
527 0 : remotePeer =
528 : () -> {
529 0 : throw e;
530 : };
531 96 : }
532 96 : return new IdentifiedUser(
533 : authConfig,
534 : realm,
535 : anonymousCowardName,
536 96 : Providers.of(canonicalUrl.get()),
537 : accountCache,
538 : groupBackend,
539 : enablePeerIPInReflogRecord,
540 : remotePeer,
541 : state,
542 : realUser);
543 : }
544 :
545 : @Override
546 : public boolean hasSameAccountId(CurrentUser other) {
547 28 : return getAccountId().get() == other.getAccountId().get();
548 : }
549 :
550 : private String guessHost() {
551 1 : String host = null;
552 1 : SocketAddress remotePeer = null;
553 : try {
554 1 : remotePeer = remotePeerProvider.get();
555 0 : } catch (OutOfScopeException | ProvisionException e) {
556 : // Leave null.
557 1 : }
558 1 : if (remotePeer instanceof InetSocketAddress) {
559 0 : InetSocketAddress sa = (InetSocketAddress) remotePeer;
560 0 : InetAddress in = sa.getAddress();
561 0 : host = in != null ? in.getHostAddress() : sa.getHostName();
562 : }
563 1 : if (Strings.isNullOrEmpty(host)) {
564 1 : return "unknown";
565 : }
566 0 : return host;
567 : }
568 : }
|