Line data Source code
1 : // Copyright (C) 2021 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 java.nio.charset.StandardCharsets.UTF_8;
18 : import static java.util.Objects.requireNonNull;
19 :
20 : import com.google.common.base.Strings;
21 : import com.google.common.collect.Iterables;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.entities.Account;
24 : import com.google.gerrit.server.account.HashedPassword;
25 : import com.google.gerrit.server.config.AuthConfig;
26 : import java.util.Set;
27 : import javax.inject.Inject;
28 : import javax.inject.Singleton;
29 : import org.eclipse.jgit.errors.ConfigInvalidException;
30 : import org.eclipse.jgit.lib.Config;
31 : import org.eclipse.jgit.lib.ObjectId;
32 :
33 : @Singleton
34 : public class ExternalIdFactory {
35 : private final ExternalIdKeyFactory externalIdKeyFactory;
36 : private AuthConfig authConfig;
37 :
38 : @Inject
39 153 : public ExternalIdFactory(ExternalIdKeyFactory externalIdKeyFactory, AuthConfig authConfig) {
40 153 : this.externalIdKeyFactory = externalIdKeyFactory;
41 153 : this.authConfig = authConfig;
42 153 : }
43 :
44 : /**
45 : * Creates an external ID.
46 : *
47 : * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
48 : * ExternalId#SCHEME_USERNAME}.
49 : * @param id the external ID, must not contain colons (':')
50 : * @param accountId the ID of the account to which the external ID belongs
51 : * @return the created external ID
52 : */
53 : public ExternalId create(String scheme, String id, Account.Id accountId) {
54 19 : return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
55 : }
56 :
57 : /**
58 : * Creates an external ID.
59 : *
60 : * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
61 : * ExternalId#SCHEME_USERNAME}.
62 : * @param id the external ID, must not contain colons (':')
63 : * @param accountId the ID of the account to which the external ID belongs
64 : * @param email the email of the external ID, may be {@code null}
65 : * @param hashedPassword the hashed password of the external ID, may be {@code null}
66 : * @return the created external ID
67 : */
68 : public ExternalId create(
69 : String scheme,
70 : String id,
71 : Account.Id accountId,
72 : @Nullable String email,
73 : @Nullable String hashedPassword) {
74 2 : return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
75 : }
76 :
77 : /**
78 : * Creates an external ID.
79 : *
80 : * @param key the external Id key
81 : * @param accountId the ID of the account to which the external ID belongs
82 : * @return the created external ID
83 : */
84 : public ExternalId create(ExternalId.Key key, Account.Id accountId) {
85 5 : return create(key, accountId, null, null);
86 : }
87 :
88 : /**
89 : * Creates an external ID.
90 : *
91 : * @param key the external Id key
92 : * @param accountId the ID of the account to which the external ID belongs
93 : * @param email the email of the external ID, may be {@code null}
94 : * @param hashedPassword the hashed password of the external ID, may be {@code null}
95 : * @return the created external ID
96 : */
97 : public ExternalId create(
98 : ExternalId.Key key,
99 : Account.Id accountId,
100 : @Nullable String email,
101 : @Nullable String hashedPassword) {
102 153 : return create(
103 153 : key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
104 : }
105 :
106 : /**
107 : * Creates an external ID adding a hashed password computed from a plain password.
108 : *
109 : * @param key the external Id key
110 : * @param accountId the ID of the account to which the external ID belongs
111 : * @param email the email of the external ID, may be {@code null}
112 : * @param plainPassword the plain HTTP password, may be {@code null}
113 : * @return the created external ID
114 : */
115 : public ExternalId createWithPassword(
116 : ExternalId.Key key,
117 : Account.Id accountId,
118 : @Nullable String email,
119 : @Nullable String plainPassword) {
120 141 : plainPassword = Strings.emptyToNull(plainPassword);
121 : String hashedPassword =
122 141 : plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
123 141 : return create(key, accountId, email, hashedPassword);
124 : }
125 :
126 : /**
127 : * Create a external ID for a username (scheme "username").
128 : *
129 : * @param id the external ID, must not contain colons (':')
130 : * @param accountId the ID of the account to which the external ID belongs
131 : * @param plainPassword the plain HTTP password, may be {@code null}
132 : * @return the created external ID
133 : */
134 : public ExternalId createUsername(
135 : String id, Account.Id accountId, @Nullable String plainPassword) {
136 140 : return createWithPassword(
137 140 : externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id),
138 : accountId,
139 : null,
140 : plainPassword);
141 : }
142 :
143 : /**
144 : * Creates an external ID with an email.
145 : *
146 : * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
147 : * ExternalId#SCHEME_USERNAME}.
148 : * @param id the external ID, must not contain colons (':')
149 : * @param accountId the ID of the account to which the external ID belongs
150 : * @param email the email of the external ID, may be {@code null}
151 : * @return the created external ID
152 : */
153 : public ExternalId createWithEmail(
154 : String scheme, String id, Account.Id accountId, @Nullable String email) {
155 146 : return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
156 : }
157 :
158 : /**
159 : * Creates an external ID with an email.
160 : *
161 : * @param key the external Id key
162 : * @param accountId the ID of the account to which the external ID belongs
163 : * @param email the email of the external ID, may be {@code null}
164 : * @return the created external ID
165 : */
166 : public ExternalId createWithEmail(
167 : ExternalId.Key key, Account.Id accountId, @Nullable String email) {
168 151 : return create(key, accountId, Strings.emptyToNull(email), null);
169 : }
170 :
171 : /**
172 : * Creates an external ID using the `mailto`-scheme.
173 : *
174 : * @param accountId the ID of the account to which the external ID belongs
175 : * @param email the email of the external ID, may be {@code null}
176 : * @return the created external ID
177 : */
178 : public ExternalId createEmail(Account.Id accountId, String email) {
179 146 : return createWithEmail(ExternalId.SCHEME_MAILTO, email, accountId, requireNonNull(email));
180 : }
181 :
182 : ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
183 151 : return create(extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
184 : }
185 :
186 : /**
187 : * Creates an external ID.
188 : *
189 : * @param key the external Id key
190 : * @param accountId the ID of the account to which the external ID belongs
191 : * @param email the email of the external ID, may be {@code null}
192 : * @param hashedPassword the hashed password of the external ID, may be {@code null}
193 : * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
194 : * {@code null} if the external ID was created in code and is not yet stored in Git.
195 : * @return the created external ID
196 : */
197 : public ExternalId create(
198 : ExternalId.Key key,
199 : Account.Id accountId,
200 : @Nullable String email,
201 : @Nullable String hashedPassword,
202 : @Nullable ObjectId blobId) {
203 153 : return ExternalId.create(
204 153 : key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
205 : }
206 :
207 : /**
208 : * Parses an external ID from a byte array that contains the external ID as a Git config file
209 : * text.
210 : *
211 : * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
212 : * email and password:
213 : *
214 : * <pre>
215 : * [externalId "username:jdoe"]
216 : * accountId = 1003407
217 : * email = jdoe@example.com
218 : * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
219 : * </pre>
220 : *
221 : * @param noteId the SHA-1 sum of the external ID used as the note's ID
222 : * @param raw a byte array that contains the external ID as a Git config file text.
223 : * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
224 : * {@code null} if the external ID was created in code and is not yet stored in Git.
225 : * @return the parsed external ID
226 : */
227 : public ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
228 : throws ConfigInvalidException {
229 151 : requireNonNull(blobId);
230 :
231 151 : Config externalIdConfig = new Config();
232 : try {
233 151 : externalIdConfig.fromText(new String(raw, UTF_8));
234 1 : } catch (ConfigInvalidException e) {
235 1 : throw invalidConfig(noteId, e.getMessage());
236 151 : }
237 :
238 151 : Set<String> externalIdKeys = externalIdConfig.getSubsections(ExternalId.EXTERNAL_ID_SECTION);
239 151 : if (externalIdKeys.size() != 1) {
240 1 : throw invalidConfig(
241 : noteId,
242 1 : String.format(
243 : "Expected exactly 1 '%s' section, found %d",
244 1 : ExternalId.EXTERNAL_ID_SECTION, externalIdKeys.size()));
245 : }
246 :
247 151 : String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
248 151 : ExternalId.Key externalIdKey = externalIdKeyFactory.parse(externalIdKeyStr);
249 151 : if (externalIdKey == null) {
250 0 : throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
251 : }
252 :
253 151 : if (!externalIdKey.sha1().getName().equals(noteId)) {
254 3 : if (!authConfig.isUserNameCaseInsensitiveMigrationMode()) {
255 3 : throw invalidConfig(
256 : noteId,
257 3 : String.format(
258 : "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
259 : }
260 :
261 2 : if (!externalIdKey.caseSensitiveSha1().getName().equals(noteId)) {
262 0 : throw invalidConfig(
263 : noteId,
264 0 : String.format(
265 : "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID"
266 : + " '%s'",
267 : externalIdKeyStr, noteId));
268 : }
269 2 : externalIdKey =
270 2 : externalIdKeyFactory.create(externalIdKey.scheme(), externalIdKey.id(), false);
271 : }
272 :
273 151 : String email =
274 151 : externalIdConfig.getString(
275 : ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.EMAIL_KEY);
276 151 : String password =
277 151 : externalIdConfig.getString(
278 : ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.PASSWORD_KEY);
279 151 : int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
280 :
281 151 : return create(
282 : externalIdKey,
283 151 : Account.id(accountId),
284 151 : Strings.emptyToNull(email),
285 151 : Strings.emptyToNull(password),
286 : blobId);
287 : }
288 :
289 : private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
290 : throws ConfigInvalidException {
291 151 : String accountIdStr =
292 151 : externalIdConfig.getString(
293 : ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY);
294 151 : if (accountIdStr == null) {
295 1 : throw invalidConfig(
296 : noteId,
297 1 : String.format(
298 : "Value for '%s.%s.%s' is missing, expected account ID",
299 : ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY));
300 : }
301 :
302 : try {
303 151 : int accountId =
304 151 : externalIdConfig.getInt(
305 : ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY, -1);
306 151 : if (accountId < 0) {
307 0 : throw invalidConfig(
308 : noteId,
309 0 : String.format(
310 : "Value %s for '%s.%s.%s' is invalid, expected account ID",
311 : accountIdStr,
312 : ExternalId.EXTERNAL_ID_SECTION,
313 : externalIdKeyStr,
314 : ExternalId.ACCOUNT_ID_KEY));
315 : }
316 151 : return accountId;
317 0 : } catch (IllegalArgumentException e) {
318 0 : ConfigInvalidException newException =
319 0 : invalidConfig(
320 : noteId,
321 0 : String.format(
322 : "Value %s for '%s.%s.%s' is invalid, expected account ID",
323 : accountIdStr,
324 : ExternalId.EXTERNAL_ID_SECTION,
325 : externalIdKeyStr,
326 : ExternalId.ACCOUNT_ID_KEY));
327 0 : newException.initCause(e);
328 0 : throw newException;
329 : }
330 : }
331 :
332 : private static ConfigInvalidException invalidConfig(String noteId, String message) {
333 3 : return new ConfigInvalidException(
334 3 : String.format("Invalid external ID config for note '%s': %s", noteId, message));
335 : }
336 : }
|