Line data Source code
1 : // Copyright (C) 2017 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;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static java.util.Objects.requireNonNull;
19 :
20 : import com.google.common.base.Strings;
21 : import com.google.common.collect.ImmutableList;
22 : import com.google.common.collect.ImmutableMap;
23 : import com.google.common.collect.ImmutableSet;
24 : import com.google.gerrit.entities.Account;
25 : import com.google.gerrit.entities.NotifyConfig.NotifyType;
26 : import com.google.gerrit.entities.RefNames;
27 : import com.google.gerrit.exceptions.DuplicateKeyException;
28 : import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
29 : import com.google.gerrit.server.account.externalids.ExternalIds;
30 : import com.google.gerrit.server.config.AllUsersName;
31 : import com.google.gerrit.server.config.CachedPreferences;
32 : import com.google.gerrit.server.git.ValidationError;
33 : import com.google.gerrit.server.git.meta.MetaDataUpdate;
34 : import com.google.gerrit.server.git.meta.VersionedMetaData;
35 : import com.google.gerrit.server.util.time.TimeUtil;
36 : import java.io.IOException;
37 : import java.time.Instant;
38 : import java.util.ArrayList;
39 : import java.util.HashMap;
40 : import java.util.List;
41 : import java.util.Map;
42 : import java.util.Optional;
43 : import java.util.Set;
44 : import org.eclipse.jgit.errors.ConfigInvalidException;
45 : import org.eclipse.jgit.lib.CommitBuilder;
46 : import org.eclipse.jgit.lib.Config;
47 : import org.eclipse.jgit.lib.ObjectId;
48 : import org.eclipse.jgit.lib.PersonIdent;
49 : import org.eclipse.jgit.lib.Ref;
50 : import org.eclipse.jgit.lib.Repository;
51 : import org.eclipse.jgit.revwalk.RevCommit;
52 : import org.eclipse.jgit.revwalk.RevSort;
53 :
54 : /**
55 : * Reads/writes account data from/to a user branch in the {@code All-Users} repository.
56 : *
57 : * <p>This is the low-level API for account creation and account updates. Most callers should use
58 : * {@link AccountsUpdate} for creating and updating accounts.
59 : *
60 : * <p>This class can read/write account properties, preferences (general, diff and edit preferences)
61 : * and project watches.
62 : *
63 : * <p>The following files are read/written:
64 : *
65 : * <ul>
66 : * <li>'account.config': Contains the account properties. Parsing and writing it is delegated to
67 : * {@link AccountProperties}.
68 : * <li>'preferences.config': Contains the preferences. Parsing and writing it is delegated to
69 : * {@link StoredPreferences}.
70 : * <li>'account.config': Contains the project watches. Parsing and writing it is delegated to
71 : * {@link ProjectWatches}.
72 : * </ul>
73 : *
74 : * <p>The commit date of the first commit on the user branch is used as registration date of the
75 : * account. The first commit may be an empty commit (since all config files are optional).
76 : */
77 : public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
78 : private final Account.Id accountId;
79 : private final AllUsersName allUsersName;
80 : private final Repository repo;
81 : private final String ref;
82 :
83 : private Optional<AccountProperties> loadedAccountProperties;
84 : private Optional<ObjectId> externalIdsRev;
85 : private ProjectWatches projectWatches;
86 : private StoredPreferences preferences;
87 151 : private Optional<AccountDelta> accountDelta = Optional.empty();
88 : private List<ValidationError> validationErrors;
89 :
90 151 : public AccountConfig(Account.Id accountId, AllUsersName allUsersName, Repository allUsersRepo) {
91 151 : this.accountId = requireNonNull(accountId, "accountId");
92 151 : this.allUsersName = requireNonNull(allUsersName, "allUsersName");
93 151 : this.repo = requireNonNull(allUsersRepo, "allUsersRepo");
94 151 : this.ref = RefNames.refsUsers(accountId);
95 151 : }
96 :
97 : @Override
98 : protected String getRefName() {
99 151 : return ref;
100 : }
101 :
102 : public AccountConfig load() throws IOException, ConfigInvalidException {
103 151 : load(allUsersName, repo);
104 151 : return this;
105 : }
106 :
107 : public AccountConfig load(ObjectId rev) throws IOException, ConfigInvalidException {
108 151 : load(allUsersName, repo, rev);
109 151 : return this;
110 : }
111 :
112 : /**
113 : * Get the loaded account.
114 : *
115 : * @return the loaded account, {@link Optional#empty()} if load didn't find the account because it
116 : * doesn't exist
117 : * @throws IllegalStateException if the account was not loaded yet
118 : */
119 : public Optional<Account> getLoadedAccount() {
120 151 : checkLoaded();
121 151 : return loadedAccountProperties.map(AccountProperties::getAccount);
122 : }
123 :
124 : /**
125 : * Returns the revision of the {@code refs/meta/external-ids} branch.
126 : *
127 : * <p>This revision can be used to load the external IDs of the loaded account lazily via {@link
128 : * ExternalIds#byAccount(com.google.gerrit.entities.Account.Id, ObjectId)}.
129 : *
130 : * @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no
131 : * {@code refs/meta/external-ids} branch exists
132 : */
133 : public Optional<ObjectId> getExternalIdsRev() {
134 151 : checkLoaded();
135 151 : return externalIdsRev;
136 : }
137 :
138 : /**
139 : * Get the project watches of the loaded account.
140 : *
141 : * @return the project watches of the loaded account
142 : */
143 : public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
144 151 : checkLoaded();
145 151 : return projectWatches.getProjectWatches();
146 : }
147 :
148 : /**
149 : * Sets the account. This means the loaded account will be overwritten with the given account.
150 : *
151 : * <p>Changing the registration date of an account is not supported.
152 : *
153 : * @param account account that should be set
154 : * @throws IllegalStateException if the account was not loaded yet
155 : */
156 : public AccountConfig setAccount(Account account) {
157 0 : checkLoaded();
158 0 : this.loadedAccountProperties =
159 0 : Optional.of(
160 0 : new AccountProperties(account.id(), account.registeredOn(), new Config(), null));
161 0 : this.accountDelta =
162 0 : Optional.of(
163 0 : AccountDelta.builder()
164 0 : .setActive(account.isActive())
165 0 : .setFullName(account.fullName())
166 0 : .setDisplayName(account.displayName())
167 0 : .setPreferredEmail(account.preferredEmail())
168 0 : .setStatus(account.status())
169 0 : .build());
170 0 : return this;
171 : }
172 :
173 : /**
174 : * Creates a new account.
175 : *
176 : * @return the new account
177 : * @throws DuplicateKeyException if the user branch already exists
178 : */
179 : public Account getNewAccount() throws DuplicateKeyException {
180 0 : return getNewAccount(TimeUtil.now());
181 : }
182 :
183 : /**
184 : * Creates a new account.
185 : *
186 : * @return the new account
187 : * @throws DuplicateKeyException if the user branch already exists
188 : */
189 : Account getNewAccount(Instant registeredOn) throws DuplicateKeyException {
190 151 : checkLoaded();
191 151 : if (revision != null) {
192 0 : throw new DuplicateKeyException(String.format("account %s already exists", accountId));
193 : }
194 151 : this.loadedAccountProperties =
195 151 : Optional.of(new AccountProperties(accountId, registeredOn, new Config(), null));
196 151 : return loadedAccountProperties.map(AccountProperties::getAccount).get();
197 : }
198 :
199 : public AccountConfig setAccountDelta(AccountDelta accountDelta) {
200 151 : this.accountDelta = Optional.of(accountDelta);
201 151 : return this;
202 : }
203 :
204 : /**
205 : * Returns the content of the {@code preferences.config} file wrapped as {@link
206 : * CachedPreferences}.
207 : */
208 : CachedPreferences asCachedPreferences() {
209 151 : checkLoaded();
210 151 : return CachedPreferences.fromConfig(preferences.getRaw());
211 : }
212 :
213 : @Override
214 : protected void onLoad() throws IOException, ConfigInvalidException {
215 151 : if (revision != null) {
216 151 : rw.reset();
217 151 : rw.markStart(revision);
218 151 : rw.sort(RevSort.REVERSE);
219 151 : Instant registeredOn = Instant.ofEpochMilli(rw.next().getCommitTime() * 1000L);
220 :
221 151 : Config accountConfig = readConfig(AccountProperties.ACCOUNT_CONFIG);
222 151 : loadedAccountProperties =
223 151 : Optional.of(new AccountProperties(accountId, registeredOn, accountConfig, revision));
224 :
225 151 : projectWatches = new ProjectWatches(accountId, readConfig(ProjectWatches.WATCH_CONFIG), this);
226 :
227 151 : preferences =
228 : new StoredPreferences(
229 : accountId,
230 151 : readConfig(StoredPreferences.PREFERENCES_CONFIG),
231 151 : StoredPreferences.readDefaultConfig(allUsersName, repo),
232 : this);
233 :
234 151 : projectWatches.parse();
235 151 : preferences.parse();
236 151 : } else {
237 151 : loadedAccountProperties = Optional.empty();
238 :
239 151 : projectWatches = new ProjectWatches(accountId, new Config(), this);
240 :
241 151 : preferences =
242 : new StoredPreferences(
243 : accountId,
244 : new Config(),
245 151 : StoredPreferences.readDefaultConfig(allUsersName, repo),
246 : this);
247 : }
248 :
249 151 : Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
250 151 : externalIdsRev = Optional.ofNullable(externalIdsRef).map(Ref::getObjectId);
251 151 : }
252 :
253 : @Override
254 : public RevCommit commit(MetaDataUpdate update) throws IOException {
255 151 : RevCommit c = super.commit(update);
256 151 : loadedAccountProperties.get().setMetaId(c);
257 151 : return c;
258 : }
259 :
260 : @Override
261 : protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
262 151 : checkLoaded();
263 :
264 151 : if (!loadedAccountProperties.isPresent()) {
265 0 : return false;
266 : }
267 :
268 151 : if (revision != null) {
269 33 : if (Strings.isNullOrEmpty(commit.getMessage())) {
270 3 : commit.setMessage("Update account\n");
271 : }
272 : } else {
273 151 : if (Strings.isNullOrEmpty(commit.getMessage())) {
274 0 : commit.setMessage("Create account\n");
275 : }
276 :
277 151 : Instant registeredOn = loadedAccountProperties.get().getRegisteredOn();
278 151 : commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
279 151 : commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
280 : }
281 :
282 151 : saveAccount();
283 151 : saveProjectWatches();
284 151 : savePreferences();
285 :
286 151 : accountDelta = Optional.empty();
287 :
288 151 : return true;
289 : }
290 :
291 : private void saveAccount() throws IOException {
292 151 : if (accountDelta.isPresent()) {
293 151 : saveConfig(
294 151 : AccountProperties.ACCOUNT_CONFIG, loadedAccountProperties.get().save(accountDelta.get()));
295 : }
296 151 : }
297 :
298 : private void saveProjectWatches() throws IOException {
299 151 : if (accountDelta.isPresent()
300 151 : && (!accountDelta.get().getDeletedProjectWatches().isEmpty()
301 151 : || !accountDelta.get().getUpdatedProjectWatches().isEmpty())) {
302 16 : Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches =
303 16 : new HashMap<>(projectWatches.getProjectWatches());
304 16 : accountDelta.get().getDeletedProjectWatches().forEach(newProjectWatches::remove);
305 16 : accountDelta.get().getUpdatedProjectWatches().forEach(newProjectWatches::put);
306 16 : saveConfig(ProjectWatches.WATCH_CONFIG, projectWatches.save(newProjectWatches));
307 : }
308 151 : }
309 :
310 : private void savePreferences() throws IOException, ConfigInvalidException {
311 151 : if (!accountDelta.isPresent()
312 151 : || (!accountDelta.get().getGeneralPreferences().isPresent()
313 151 : && !accountDelta.get().getDiffPreferences().isPresent()
314 151 : && !accountDelta.get().getEditPreferences().isPresent())) {
315 151 : return;
316 : }
317 :
318 8 : saveConfig(
319 : StoredPreferences.PREFERENCES_CONFIG,
320 8 : preferences.saveGeneralPreferences(
321 8 : accountDelta.get().getGeneralPreferences(),
322 8 : accountDelta.get().getDiffPreferences(),
323 8 : accountDelta.get().getEditPreferences()));
324 8 : }
325 :
326 : private void checkLoaded() {
327 151 : checkState(loadedAccountProperties != null, "Account %s not loaded yet", accountId.get());
328 151 : }
329 :
330 : /**
331 : * Get the validation errors, if any were discovered during parsing the account data.
332 : *
333 : * @return list of errors; empty list if there are no errors.
334 : */
335 : public List<ValidationError> getValidationErrors() {
336 2 : if (validationErrors != null) {
337 1 : return ImmutableList.copyOf(validationErrors);
338 : }
339 2 : return ImmutableList.of();
340 : }
341 :
342 : @Override
343 : public void error(ValidationError error) {
344 1 : if (validationErrors == null) {
345 1 : validationErrors = new ArrayList<>(4);
346 : }
347 1 : validationErrors.add(error);
348 1 : }
349 : }
|