diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt index e7d59fb7bc..7970084d88 100644 --- a/Documentation/cmd-index.txt +++ b/Documentation/cmd-index.txt @@ -96,6 +96,9 @@ review. See link:user-upload.html#push_create[Creating Changes]. link:cmd-create-account.html[gerrit create-account]:: Create a new batch/role account. +link:cmd-set-account.html[gerrit set-account]:: + Change an account's settings. + link:cmd-create-group.html[gerrit create-group]:: Create a new account group. diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt new file mode 100644 index 0000000000..5719a9c3c3 --- /dev/null +++ b/Documentation/cmd-set-account.txt @@ -0,0 +1,92 @@ +gerrit set-account +================== + +NAME +---- +gerrit set-account - Change an account's settings. + +SYNOPSIS +-------- +[verse] +set-account [--full-name ] [--active|--inactive] \ + [--add-email ] [--delete-email | ALL] \ + [--add-ssh-key - | ] \ + [--delete-ssh-key - | | ALL] + +DESCRIPTION +----------- +Modifies a given user's settings. This command can be useful to +deactivate an account or add/delete ssh keys without going through +the UI. + +It also allows managing email addresses, which bypasses the +verification step we force within the UI. + +ACCESS +------ +Caller must be a member of the privileged 'Administrators' group. + +SCRIPTING +--------- +This command is intended to be used in scripts. + +OPTIONS +------- +:: + Required; Full name, email-address, SSH username or account id. + +--full-name:: + Display name of the user account. ++ +Names containing spaces should be quoted in single quotes ('). +This most likely requires double quoting the value, for example +`--full-name "'A description string'"`. + +--active:: + Set the account state to be active. + +--inactive:: + Set the account state to be inactive. This prevents the + user from logging in. + +--add-email:: + Add another email to the user's account. This doesn't + trigger the mail validation and adds the email directly + to the user's account. + May be supplied more than once to add multiple emails to + an account in a single command execution. + +--delete-email:: + Delete an email from this user's account if it exists. + If the email provided is 'ALL', all associated emails are + deleted from this account. + Maybe supplied more than once to remove multiple emails + from an account in a single command execution. + +--add-ssh-key:: + Content of the public SSH key to add to the account's + keyring. If `-` the key is read from stdin, rather than + from the command line. + May be supplied more than once to add multiple SSH keys + in a single command execution. + +--delete-ssh-key:: + Content of the public SSH key to remove from the account's + keyring or the comment associated with this key. + If `-` the key is read from stdin, rather than from the + command line. If the key provided is 'ALL', all + associated SSH keys are removed from this account. + May be supplied more than once to delete multiple SSH + keys in a single command execution. + +EXAMPLES +-------- +Add an email and SSH key to `watcher`'s account: + +==== + $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit set-account --add-ssh-key - --add-email mail@example.com watcher +==== + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java index 34f64dae0e..9eeaf74536 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java @@ -36,5 +36,6 @@ public class MasterCommandModule extends CommandModule { command(gerrit, "replicate").to(Replicate.class); command(gerrit, "set-project-parent").to(AdminSetParent.class); command(gerrit, "review").to(ReviewCommand.class); + command(gerrit, "set-account").to(SetAccountCommand.class); } } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java new file mode 100644 index 0000000000..9cf3586a4f --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java @@ -0,0 +1,273 @@ +// Copyright (C) 2012 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.sshd.commands; + + +import com.google.gerrit.common.errors.InvalidSshKeyException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Account.FieldName; +import com.google.gerrit.reviewdb.client.AccountExternalId; +import com.google.gerrit.reviewdb.client.AccountSshKey; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountException; +import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AuthRequest; +import com.google.gerrit.server.account.Realm; +import com.google.gerrit.server.ssh.SshKeyCache; +import com.google.gerrit.sshd.BaseCommand; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; + +import org.apache.sshd.server.Environment; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Set a user's account settings. **/ +final class SetAccountCommand extends BaseCommand { + + @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id") + private Account.Id id; + + @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account") + private String fullName; + + @Option(name = "--active", usage = "set account's state to active") + private boolean active; + + @Option(name = "--inactive", usage = "set account's state to inactive") + private boolean inactive; + + @Option(name = "--add-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to add to the account") + private List addEmails = new ArrayList(); + + @Option(name = "--delete-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to delete from the account") + private List deleteEmails = new ArrayList(); + + @Option(name = "--add-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to add to the account") + private List addSshKeys = new ArrayList(); + + @Option(name = "--delete-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to delete from the account") + private List deleteSshKeys = new ArrayList(); + + @Inject + private IdentifiedUser currentUser; + + @Inject + private ReviewDb db; + + @Inject + private AccountManager manager; + + @Inject + private SshKeyCache sshKeyCache; + + @Inject + private AccountCache byIdCache; + + @Inject + private Realm realm; + + @Override + public void start(final Environment env) { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + if (!currentUser.getCapabilities().canAdministrateServer()) { + String msg = + String.format( + "fatal: %s does not have \"Administrator\" capability.", + currentUser.getUserName()); + throw new UnloggedFailure(1, msg); + } + parseCommandLine(); + validate(); + setAccount(); + } + }); + } + + private void validate() throws UnloggedFailure { + if (active && inactive) { + throw new UnloggedFailure(1, + "--active and --inactive options are mutually exclusive."); + } + if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) { + throw new UnloggedFailure(1, "Only one option may use the stdin"); + } + if (deleteSshKeys.contains("ALL")) { + deleteSshKeys = Collections.singletonList("ALL"); + } + if (deleteEmails.contains("ALL")) { + deleteEmails = Collections.singletonList("ALL"); + } + } + + private void setAccount() throws OrmException, IOException, UnloggedFailure { + + final Account account = db.accounts().get(id); + boolean accountUpdated = false; + boolean sshKeysUpdated = false; + + for (String email : addEmails) { + link(id, email); + } + + for (String email : deleteEmails) { + deleteMail(id, email); + } + + if (fullName != null) { + if (realm.allowsEdit(FieldName.FULL_NAME)) { + account.setFullName(fullName); + } else { + throw new UnloggedFailure(1, "The realm doesn't allow editing names"); + } + } + + if (active) { + accountUpdated = true; + account.setActive(true); + } else if (inactive) { + accountUpdated = true; + account.setActive(false); + } + + addSshKeys = readSshKey(addSshKeys); + if (!addSshKeys.isEmpty()) { + sshKeysUpdated = true; + addSshKeys(addSshKeys, account); + } + + deleteSshKeys = readSshKey(deleteSshKeys); + if (!deleteSshKeys.isEmpty()) { + sshKeysUpdated = true; + deleteSshKeys(deleteSshKeys, account); + } + + if (accountUpdated) { + db.accounts().update(Collections.singleton(account)); + byIdCache.evict(id); + } + + if (sshKeysUpdated) { + sshKeyCache.evict(account.getUserName()); + } + + db.close(); + } + + private void addSshKeys(final List keys, final Account account) + throws OrmException, UnloggedFailure { + List accountKeys = new ArrayList(); + int seq = db.accountSshKeys().byAccount(account.getId()).toList().size(); + for (String key : keys) { + try { + seq++; + AccountSshKey accountSshKey = sshKeyCache.create( + new AccountSshKey.Id(account.getId(), seq), key.trim()); + accountKeys.add(accountSshKey); + } catch (InvalidSshKeyException e) { + throw new UnloggedFailure(1, "fatal: invalid ssh key"); + } + } + db.accountSshKeys().insert(accountKeys); + } + + private void deleteSshKeys(final List keys, final Account account) + throws OrmException { + ResultSet allKeys = db.accountSshKeys().byAccount(account.getId()); + if (keys.contains("ALL")) { + db.accountSshKeys().delete(allKeys); + } else { + List accountKeys = new ArrayList(); + for (String key : keys) { + for (AccountSshKey accountSshKey : allKeys) { + if (key.trim().equals(accountSshKey.getSshPublicKey()) + || accountSshKey.getComment().trim().equals(key)) { + accountKeys.add(accountSshKey); + } + } + } + db.accountSshKeys().delete(accountKeys); + } + } + + private void deleteMail(Account.Id id, final String mailAddress) + throws UnloggedFailure, OrmException { + if (mailAddress.equals("ALL")) { + ResultSet ids = db.accountExternalIds().byAccount(id); + for (AccountExternalId extId : ids) { + if (extId.isScheme(AccountExternalId.SCHEME_MAILTO)) { + unlink(id, extId.getEmailAddress()); + } + } + } else { + AccountExternalId.Key key = new AccountExternalId.Key( + AccountExternalId.SCHEME_MAILTO, mailAddress); + AccountExternalId extId = db.accountExternalIds().get(key); + if (extId != null) { + unlink(id, mailAddress); + } + } + } + + private void unlink(Account.Id id, final String mailAddress) + throws UnloggedFailure { + try { + manager.unlink(id, AuthRequest.forEmail(mailAddress)); + } catch (AccountException ex) { + throw die(ex.getMessage()); + } + } + + private void link(Account.Id id, final String mailAddress) + throws UnloggedFailure { + try { + manager.link(id, AuthRequest.forEmail(mailAddress)); + } catch (AccountException ex) { + throw die(ex.getMessage()); + } + } + + private List readSshKey(final List sshKeys) + throws UnsupportedEncodingException, IOException { + if (!sshKeys.isEmpty()) { + String sshKey = ""; + int idx = sshKeys.indexOf("-"); + if (idx >= 0) { + sshKey = ""; + BufferedReader br = + new BufferedReader(new InputStreamReader(in, "UTF-8")); + String line; + while ((line = br.readLine()) != null) { + sshKey += line + "\n"; + } + sshKeys.set(idx, sshKey); + } + } + return sshKeys; + } +}