Line data Source code
1 : // Copyright (C) 2016 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.account.externalids;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
19 : import static java.nio.charset.StandardCharsets.UTF_8;
20 :
21 : import com.google.auto.value.AutoValue;
22 : import com.google.auto.value.extension.memoized.Memoized;
23 : import com.google.common.annotations.VisibleForTesting;
24 : import com.google.common.base.Strings;
25 : import com.google.common.collect.ImmutableSet;
26 : import com.google.common.hash.Hashing;
27 : import com.google.gerrit.common.Nullable;
28 : import com.google.gerrit.entities.Account;
29 : import com.google.gerrit.extensions.client.AuthType;
30 : import com.google.gerrit.git.ObjectIds;
31 : import java.io.Serializable;
32 : import java.util.Collection;
33 : import java.util.Locale;
34 : import java.util.Objects;
35 : import java.util.Optional;
36 : import java.util.regex.Pattern;
37 : import java.util.stream.Stream;
38 : import org.eclipse.jgit.lib.Config;
39 : import org.eclipse.jgit.lib.ObjectId;
40 :
41 : @AutoValue
42 153 : public abstract class ExternalId implements Serializable {
43 : // If these regular expressions are modified the same modifications should be done to the
44 : // corresponding regular expressions in the
45 : // com.google.gerrit.client.account.UsernameField class.
46 : private static final String USER_NAME_PATTERN_FIRST_REGEX = "[a-zA-Z0-9]";
47 : private static final String USER_NAME_PATTERN_REST_REGEX = "[a-zA-Z0-9.!#$%&’*+=?^_`\\{|\\}~@-]";
48 : private static final String USER_NAME_PATTERN_LAST_REGEX = "[a-zA-Z0-9]";
49 :
50 : /** Regular expression that a username must match. */
51 : private static final String USER_NAME_PATTERN_REGEX =
52 : "^("
53 : + //
54 : USER_NAME_PATTERN_FIRST_REGEX
55 : + //
56 : USER_NAME_PATTERN_REST_REGEX
57 : + "*"
58 : + //
59 : USER_NAME_PATTERN_LAST_REGEX
60 : + //
61 : "|"
62 : + //
63 : USER_NAME_PATTERN_FIRST_REGEX
64 : + //
65 : ")$";
66 :
67 153 : private static final Pattern USER_NAME_PATTERN = Pattern.compile(USER_NAME_PATTERN_REGEX);
68 :
69 : public static boolean isValidUsername(String username) {
70 63 : return USER_NAME_PATTERN.matcher(username).matches();
71 : }
72 :
73 : /**
74 : * Returns the ID of the first external ID from the provided external IDs that has the {@link
75 : * ExternalId#SCHEME_USERNAME} scheme.
76 : *
77 : * @param extIds external IDs
78 : * @return the ID of the first external ID from the provided external IDs that has the {@link
79 : * ExternalId#SCHEME_USERNAME} scheme
80 : */
81 : public static Optional<String> getUserName(Collection<ExternalId> extIds) {
82 152 : return extIds.stream()
83 152 : .filter(e -> e.isScheme(SCHEME_USERNAME))
84 152 : .map(e -> e.key().id())
85 152 : .filter(u -> !Strings.isNullOrEmpty(u))
86 152 : .findFirst();
87 : }
88 :
89 : /**
90 : * Returns all IDs of the provided external IDs that have the {@link ExternalId#SCHEME_MAILTO}
91 : * scheme as a distinct stream.
92 : *
93 : * @param extIds external IDs
94 : * @return distinct stream of all IDs of the provided external IDs that have the {@link
95 : * ExternalId#SCHEME_MAILTO} scheme
96 : */
97 : public static Stream<String> getEmails(Collection<ExternalId> extIds) {
98 135 : return extIds.stream().filter(e -> e.isScheme(SCHEME_MAILTO)).map(e -> e.key().id()).distinct();
99 : }
100 :
101 : private static final long serialVersionUID = 1L;
102 :
103 : static final String EXTERNAL_ID_SECTION = "externalId";
104 : static final String ACCOUNT_ID_KEY = "accountId";
105 : static final String EMAIL_KEY = "email";
106 : static final String PASSWORD_KEY = "password";
107 :
108 : /**
109 : * Scheme used to label accounts created, when using the LDAP-based authentication types {@link
110 : * AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link AuthType#HTTP_LDAP}, and {@link
111 : * AuthType#LDAP_BIND}. The external ID stores the username. Accounts with such an external ID
112 : * will be authenticated against the configured LDAP identity provider.
113 : *
114 : * <p>The name {@code gerrit:} was a very poor choice.
115 : *
116 : * <p>Scheme names must not contain colons (':').
117 : *
118 : * <p>Will be handled case insensitive, if auth.userNameCaseInsensitive = true.
119 : */
120 : public static final String SCHEME_GERRIT = "gerrit";
121 :
122 : /** Scheme used for randomly created identities constructed by a UUID. */
123 : public static final String SCHEME_UUID = "uuid";
124 :
125 : /** Scheme used to represent only an email address. */
126 : public static final String SCHEME_MAILTO = "mailto";
127 :
128 : /**
129 : * Scheme for the username used to authenticate an account, e.g. over SSH.
130 : *
131 : * <p>Will be handled case insensitive, if auth.userNameCaseInsensitive = true.
132 : */
133 : public static final String SCHEME_USERNAME = "username";
134 :
135 : /** Scheme used for GPG public keys. */
136 : public static final String SCHEME_GPGKEY = "gpgkey";
137 :
138 : /** Scheme for imported accounts from other servers with different GerritServerId */
139 : public static final String SCHEME_IMPORTED = "imported";
140 :
141 : /** Scheme for external auth used during authentication, e.g. OAuth Token */
142 : public static final String SCHEME_EXTERNAL = "external";
143 :
144 : /** Scheme for http resources. OpenID in particular makes use of these external IDs. */
145 : public static final String SCHEME_HTTP = "http";
146 :
147 : /** Scheme for https resources. OpenID in particular makes use of these external IDs. */
148 : public static final String SCHEME_HTTPS = "https";
149 :
150 : /** Scheme for xri resources. OpenID in particular makes use of these external IDs. */
151 : public static final String SCHEME_XRI = "xri";
152 :
153 : @AutoValue
154 153 : public abstract static class Key implements Serializable {
155 : private static final long serialVersionUID = 1L;
156 :
157 : /**
158 : * Creates an external ID key.
159 : *
160 : * @param scheme the scheme name, must not contain colons (':'), can be {@code null}
161 : * @param id the external ID, must not contain colons (':')
162 : * @param isCaseInsensitive whether the external ID key is matched case insensitively
163 : * @return the created external ID key
164 : */
165 : @VisibleForTesting
166 : public static Key create(@Nullable String scheme, String id, boolean isCaseInsensitive) {
167 153 : return new AutoValue_ExternalId_Key(Strings.emptyToNull(scheme), id, isCaseInsensitive);
168 : }
169 :
170 : /**
171 : * Parses an external ID key from a string in the format "scheme:id" or "id".
172 : *
173 : * @return the parsed external ID key
174 : */
175 : @VisibleForTesting
176 : public static Key parse(String externalId, boolean isCaseInsensitive) {
177 3 : int c = externalId.indexOf(':');
178 3 : if (c < 1 || c >= externalId.length() - 1) {
179 1 : return create(null, externalId, isCaseInsensitive);
180 : }
181 3 : return create(externalId.substring(0, c), externalId.substring(c + 1), isCaseInsensitive);
182 : }
183 :
184 : public abstract @Nullable String scheme();
185 :
186 : public abstract String id();
187 :
188 : public abstract boolean isCaseInsensitive();
189 :
190 : public boolean isScheme(String scheme) {
191 153 : return scheme.equals(scheme());
192 : }
193 :
194 : @Memoized
195 : public ObjectId sha1() {
196 152 : return sha1(isCaseInsensitive());
197 : }
198 :
199 : /**
200 : * Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids
201 : * notes branch.
202 : */
203 : @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
204 : private ObjectId sha1(Boolean isCaseInsensitive) {
205 152 : String keyString = isCaseInsensitive ? get().toLowerCase(Locale.US) : get();
206 152 : return ObjectId.fromRaw(Hashing.sha1().hashString(keyString, UTF_8).asBytes());
207 : }
208 :
209 : @Memoized
210 : public ObjectId caseSensitiveSha1() {
211 2 : return sha1(false);
212 : }
213 :
214 : /**
215 : * Exports this external ID key as string with the format "scheme:id", or "id" if scheme is
216 : * null.
217 : *
218 : * <p>This string representation is used as subsection name in the Git config file that stores
219 : * the external ID.
220 : */
221 : public String get() {
222 152 : if (scheme() != null) {
223 152 : return scheme() + ":" + id();
224 : }
225 1 : return id();
226 : }
227 :
228 : @Override
229 : public final String toString() {
230 5 : return get();
231 : }
232 :
233 : @Override
234 : public final boolean equals(Object obj) {
235 152 : if (!(obj instanceof ExternalId.Key)) {
236 0 : return false;
237 : }
238 152 : ExternalId.Key o = (ExternalId.Key) obj;
239 :
240 152 : return sha1().equals(o.sha1());
241 : }
242 :
243 : @Override
244 : @Memoized
245 : public int hashCode() {
246 151 : return Objects.hash(sha1());
247 : }
248 :
249 : public static ImmutableSet<ExternalId.Key> from(Collection<ExternalId> extIds) {
250 151 : return extIds.stream().map(ExternalId::key).collect(toImmutableSet());
251 : }
252 : }
253 :
254 : @VisibleForTesting
255 : public static ExternalId create(
256 : Key key,
257 : Account.Id accountId,
258 : @Nullable String email,
259 : @Nullable String hashedPassword,
260 : @Nullable ObjectId blobId) {
261 153 : return new AutoValue_ExternalId(
262 : key,
263 : accountId,
264 153 : key.isCaseInsensitive(),
265 153 : Strings.emptyToNull(email),
266 153 : Strings.emptyToNull(hashedPassword),
267 : blobId);
268 : }
269 :
270 : public abstract Key key();
271 :
272 : public abstract Account.Id accountId();
273 :
274 : public abstract boolean isCaseInsensitive();
275 :
276 : public abstract @Nullable String email();
277 :
278 : public abstract @Nullable String password();
279 :
280 : /**
281 : * ID of the note blob in the external IDs branch that stores this external ID. {@code null} if
282 : * the external ID was created in code and is not yet stored in Git.
283 : */
284 : public abstract @Nullable ObjectId blobId();
285 :
286 : public void checkThatBlobIdIsSet() {
287 151 : checkState(blobId() != null, "No blob ID set for external ID %s", key().get());
288 151 : }
289 :
290 : public boolean isScheme(String scheme) {
291 153 : return key().isScheme(scheme);
292 : }
293 :
294 : public byte[] toByteArray() {
295 11 : checkState(blobId() != null, "Missing blobId in external ID %s", key().get());
296 11 : byte[] b = new byte[2 * ObjectIds.STR_LEN + 1];
297 11 : key().sha1().copyTo(b, 0);
298 11 : b[ObjectIds.STR_LEN] = ':';
299 11 : blobId().copyTo(b, ObjectIds.STR_LEN + 1);
300 11 : return b;
301 : }
302 :
303 : /**
304 : * For checking if two external IDs are equals the blobId is excluded and external IDs that have
305 : * different blob IDs but identical other fields are considered equal. This way an external ID
306 : * that was loaded from Git can be equal with an external ID that was created from code.
307 : */
308 : @Override
309 : public final boolean equals(Object obj) {
310 106 : if (!(obj instanceof ExternalId)) {
311 0 : return false;
312 : }
313 106 : ExternalId o = (ExternalId) obj;
314 106 : return Objects.equals(key(), o.key())
315 4 : && Objects.equals(accountId(), o.accountId())
316 4 : && isCaseInsensitive() == o.isCaseInsensitive()
317 4 : && Objects.equals(email(), o.email())
318 106 : && Objects.equals(password(), o.password());
319 : }
320 :
321 : @Memoized
322 : @Override
323 : public int hashCode() {
324 151 : return Objects.hash(key(), accountId(), isCaseInsensitive(), email(), password());
325 : }
326 :
327 : /**
328 : * Exports this external ID as Git config file text.
329 : *
330 : * <p>The Git config has exactly one externalId subsection with an accountId and optionally email
331 : * and password:
332 : *
333 : * <pre>
334 : * [externalId "username:jdoe"]
335 : * accountId = 1003407
336 : * email = jdoe@example.com
337 : * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
338 : * </pre>
339 : */
340 : @Override
341 : @Memoized
342 : public String toString() {
343 14 : Config c = new Config();
344 14 : writeToConfig(c);
345 14 : return c.toText();
346 : }
347 :
348 : public void writeToConfig(Config c) {
349 151 : String externalIdKey = key().get();
350 : // Do not use c.setInt(...) to write the account ID because c.setInt(...) persists integers
351 : // that can be expressed in KiB as a unit strings, e.g. "1024000" is stored as "100k". Using
352 : // c.setString(...) ensures that account IDs are human readable.
353 151 : c.setString(
354 151 : EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, Integer.toString(accountId().get()));
355 :
356 151 : if (email() != null) {
357 150 : c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email());
358 : } else {
359 151 : c.unset(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY);
360 : }
361 :
362 151 : if (password() != null) {
363 139 : c.setString(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY, password());
364 : } else {
365 151 : c.unset(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY);
366 : }
367 151 : }
368 : }
|