blob: b20aa0be611aec20cc371f5fbc71c73c13489bbf [file] [log] [blame]
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
/**
* ‘account.config’ file in the user branch in the All-Users repository that contains the properties
* of the account.
*
* <p>The 'account.config' file is a git config file that has one 'account' section with the
* properties of the account:
*
* <pre>
* [account]
* active = false
* fullName = John Doe
* preferredEmail = john.doe@foo.com
* status = Overloaded with reviews
* </pre>
*
* <p>All keys are optional. This means 'account.config' may not exist on the user branch if no
* properties are set.
*
* <p>Not setting a key and setting a key to an empty string are treated the same way and result in
* a {@code null} value.
*
* <p>If no value for 'active' is specified, by default the account is considered as active.
*
* <p>The commit date of the first commit on the user branch is used as registration date of the
* account. The first commit may be an empty commit (if no properties were set and 'account.config'
* doesn't exist).
*/
public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
public static final String ACCOUNT_CONFIG = "account.config";
public static final String ACCOUNT = "account";
public static final String KEY_ACTIVE = "active";
public static final String KEY_FULL_NAME = "fullName";
public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
public static final String KEY_STATUS = "status";
private final Account.Id accountId;
private final Repository repo;
private final String ref;
private Optional<Account> loadedAccount;
private Optional<ObjectId> externalIdsRev;
private WatchConfig watchConfig;
private PreferencesConfig prefConfig;
private Optional<InternalAccountUpdate> accountUpdate = Optional.empty();
private Timestamp registeredOn;
private boolean eagerParsing;
private List<ValidationError> validationErrors;
public AccountConfig(Account.Id accountId, Repository allUsersRepo) {
this.accountId = checkNotNull(accountId, "accountId");
this.repo = checkNotNull(allUsersRepo, "allUsersRepo");
this.ref = RefNames.refsUsers(accountId);
}
/**
* Sets whether all account data should be eagerly parsed.
*
* <p>Eager parsing should only be used if the caller is interested in validation errors for all
* account data (see {@link #getValidationErrors()}.
*
* @param eagerParsing whether all account data should be eagerly parsed
* @return this AccountConfig instance for chaining
*/
public AccountConfig setEagerParsing(boolean eagerParsing) {
checkState(loadedAccount == null, "Account %s already loaded", accountId.get());
this.eagerParsing = eagerParsing;
return this;
}
@Override
protected String getRefName() {
return ref;
}
public AccountConfig load() throws IOException, ConfigInvalidException {
load(repo);
return this;
}
/**
* Get the loaded account.
*
* @return the loaded account, {@link Optional#empty()} if load didn't find the account because it
* doesn't exist
* @throws IllegalStateException if the account was not loaded yet
*/
public Optional<Account> getLoadedAccount() {
checkLoaded();
return loadedAccount;
}
/**
* Returns the revision of the {@code refs/meta/external-ids} branch.
*
* <p>This revision can be used to load the external IDs of the loaded account lazily via {@link
* ExternalIds#byAccount(com.google.gerrit.reviewdb.client.Account.Id, ObjectId)}.
*
* @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no
* {@code refs/meta/external-ids} branch exists
*/
public Optional<ObjectId> getExternalIdsRev() {
checkLoaded();
return externalIdsRev;
}
/**
* Get the project watches of the loaded account.
*
* @return the project watches of the loaded account
*/
public Map<ProjectWatchKey, Set<NotifyType>> getProjectWatches() {
checkLoaded();
return watchConfig.getProjectWatches();
}
/**
* Get the general preferences of the loaded account.
*
* @return the general preferences of the loaded account
*/
public GeneralPreferencesInfo getGeneralPreferences() {
checkLoaded();
return prefConfig.getGeneralPreferences();
}
/**
* Sets the account. This means the loaded account will be overwritten with the given account.
*
* <p>Changing the registration date of an account is not supported.
*
* @param account account that should be set
* @throws IllegalStateException if the account was not loaded yet
*/
public AccountConfig setAccount(Account account) {
checkLoaded();
this.loadedAccount = Optional.of(account);
this.accountUpdate =
Optional.of(
InternalAccountUpdate.builder()
.setActive(account.isActive())
.setFullName(account.getFullName())
.setPreferredEmail(account.getPreferredEmail())
.setStatus(account.getStatus())
.build());
this.registeredOn = account.getRegisteredOn();
return this;
}
/**
* Creates a new account.
*
* @return the new account
* @throws OrmDuplicateKeyException if the user branch already exists
*/
public Account getNewAccount() throws OrmDuplicateKeyException {
return getNewAccount(TimeUtil.nowTs());
}
/**
* Creates a new account.
*
* @return the new account
* @throws OrmDuplicateKeyException if the user branch already exists
*/
Account getNewAccount(Timestamp registeredOn) throws OrmDuplicateKeyException {
checkLoaded();
if (revision != null) {
throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId));
}
this.registeredOn = registeredOn;
this.loadedAccount = Optional.of(new Account(accountId, registeredOn));
return loadedAccount.get();
}
public AccountConfig setAccountUpdate(InternalAccountUpdate accountUpdate) {
this.accountUpdate = Optional.of(accountUpdate);
return this;
}
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
if (revision != null) {
rw.reset();
rw.markStart(revision);
rw.sort(RevSort.REVERSE);
registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);
Config accountConfig = readConfig(ACCOUNT_CONFIG);
loadedAccount = Optional.of(parse(accountConfig, revision.name()));
Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
externalIdsRev =
externalIdsRef != null ? Optional.of(externalIdsRef.getObjectId()) : Optional.empty();
watchConfig = new WatchConfig(accountId, readConfig(WatchConfig.WATCH_CONFIG), this);
prefConfig =
new PreferencesConfig(
accountId,
readConfig(PreferencesConfig.PREFERENCES_CONFIG),
PreferencesConfig.readDefaultConfig(repo),
this);
if (eagerParsing) {
watchConfig.parse();
prefConfig.parse();
}
} else {
loadedAccount = Optional.empty();
}
}
private Account parse(Config cfg, String metaId) {
Account account = new Account(accountId, registeredOn);
account.setActive(cfg.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
account.setFullName(get(cfg, KEY_FULL_NAME));
String preferredEmail = get(cfg, KEY_PREFERRED_EMAIL);
account.setPreferredEmail(preferredEmail);
account.setStatus(get(cfg, KEY_STATUS));
account.setMetaId(metaId);
return account;
}
@Override
public RevCommit commit(MetaDataUpdate update) throws IOException {
RevCommit c = super.commit(update);
loadedAccount.get().setMetaId(c.name());
return c;
}
@Override
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
checkLoaded();
if (!loadedAccount.isPresent()) {
return false;
}
if (revision != null) {
if (Strings.isNullOrEmpty(commit.getMessage())) {
commit.setMessage("Update account\n");
}
} else {
if (Strings.isNullOrEmpty(commit.getMessage())) {
commit.setMessage("Create account\n");
}
commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
}
Config accountConfig = saveAccount();
saveProjectWatches();
saveGeneralPreferences();
// metaId is set in the commit(MetaDataUpdate) method after the commit is created
loadedAccount = Optional.of(parse(accountConfig, null));
accountUpdate = Optional.empty();
return true;
}
private Config saveAccount() throws IOException, ConfigInvalidException {
Config accountConfig = readConfig(ACCOUNT_CONFIG);
if (accountUpdate.isPresent()) {
writeToAccountConfig(accountUpdate.get(), accountConfig);
}
saveConfig(ACCOUNT_CONFIG, accountConfig);
return accountConfig;
}
public static void writeToAccountConfig(InternalAccountUpdate accountUpdate, Config cfg) {
accountUpdate.getActive().ifPresent(active -> setActive(cfg, active));
accountUpdate.getFullName().ifPresent(fullName -> set(cfg, KEY_FULL_NAME, fullName));
accountUpdate
.getPreferredEmail()
.ifPresent(preferredEmail -> set(cfg, KEY_PREFERRED_EMAIL, preferredEmail));
accountUpdate.getStatus().ifPresent(status -> set(cfg, KEY_STATUS, status));
}
private void saveProjectWatches() throws IOException {
if (accountUpdate.isPresent()
&& (!accountUpdate.get().getDeletedProjectWatches().isEmpty()
|| !accountUpdate.get().getUpdatedProjectWatches().isEmpty())) {
Map<ProjectWatchKey, Set<NotifyType>> projectWatches = watchConfig.getProjectWatches();
accountUpdate.get().getDeletedProjectWatches().forEach(pw -> projectWatches.remove(pw));
accountUpdate
.get()
.getUpdatedProjectWatches()
.forEach((pw, nt) -> projectWatches.put(pw, nt));
saveConfig(WatchConfig.WATCH_CONFIG, watchConfig.save(projectWatches));
}
}
private void saveGeneralPreferences() throws IOException, ConfigInvalidException {
if (accountUpdate.isPresent() && accountUpdate.get().getGeneralPreferences().isPresent()) {
saveConfig(
PreferencesConfig.PREFERENCES_CONFIG,
prefConfig.saveGeneralPreferences(accountUpdate.get().getGeneralPreferences().get()));
}
}
/**
* Sets/Unsets {@code account.active} in the given config.
*
* <p>{@code account.active} is set to {@code false} if the account is inactive.
*
* <p>If the account is active {@code account.active} is unset since {@code true} is the default
* if this field is missing.
*
* @param cfg the config
* @param value whether the account is active
*/
private static void setActive(Config cfg, boolean value) {
if (!value) {
cfg.setBoolean(ACCOUNT, null, KEY_ACTIVE, false);
} else {
cfg.unset(ACCOUNT, null, KEY_ACTIVE);
}
}
/**
* Sets/Unsets the given key in the given config.
*
* <p>The key unset if the value is {@code null}.
*
* @param cfg the config
* @param key the key
* @param value the value
*/
private static void set(Config cfg, String key, String value) {
if (!Strings.isNullOrEmpty(value)) {
cfg.setString(ACCOUNT, null, key, value);
} else {
cfg.unset(ACCOUNT, null, key);
}
}
/**
* Gets the given key from the given config.
*
* <p>Empty values are returned as {@code null}
*
* @param cfg the config
* @param key the key
* @return the value, {@code null} if key was not set or key was set to empty string
*/
private static String get(Config cfg, String key) {
return Strings.emptyToNull(cfg.getString(ACCOUNT, null, key));
}
private void checkLoaded() {
checkState(loadedAccount != null, "Account %s not loaded yet", accountId.get());
}
/**
* Get the validation errors, if any were discovered during parsing the account data.
*
* <p>To get validation errors for all account data request eager parsing before loading the
* account (see {@link #setEagerParsing(boolean)}).
*
* @return list of errors; empty list if there are no errors.
*/
public List<ValidationError> getValidationErrors() {
if (validationErrors != null) {
return ImmutableList.copyOf(validationErrors);
}
return ImmutableList.of();
}
@Override
public void error(ValidationError error) {
if (validationErrors == null) {
validationErrors = new ArrayList<>(4);
}
validationErrors.add(error);
}
}