SSH-command : set-account

Added a new command, 'set-account', that can be used to edit
various settings of an account, such as active status,
emails, ssh keys and the full name.

The commandline pattern:
set-account [--full-name <FULLNAME>] [--active|--inactive]
            [--add-email <EMAIL>] [--delete-email <EMAIL> | ALL]
            [--add-ssh-key - | <KEY>]
            [--delete-ssh-key - | <KEY> | ALL] <USER>

Bug: issue 1095
Change-Id: Ice168daad59db3812b2db7e5f055931ebc073df7
This commit is contained in:
Deniz Turkoglu 2012-04-13 15:26:01 +02:00 committed by Deniz Türkoglu
parent 90ff393d02
commit 2c4bbcffc3
4 changed files with 369 additions and 0 deletions

View File

@ -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.

View File

@ -0,0 +1,92 @@
gerrit set-account
==================
NAME
----
gerrit set-account - Change an account's settings.
SYNOPSIS
--------
[verse]
set-account [--full-name <FULLNAME>] [--active|--inactive] \
[--add-email <EMAIL>] [--delete-email <EMAIL> | ALL] \
[--add-ssh-key - | <KEY>] \
[--delete-ssh-key - | <KEY> | ALL] <USER>
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
-------
<USER>::
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]

View File

@ -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);
}
}

View File

@ -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<String> addEmails = new ArrayList<String>();
@Option(name = "--delete-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to delete from the account")
private List<String> deleteEmails = new ArrayList<String>();
@Option(name = "--add-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to add to the account")
private List<String> addSshKeys = new ArrayList<String>();
@Option(name = "--delete-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to delete from the account")
private List<String> deleteSshKeys = new ArrayList<String>();
@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<String> keys, final Account account)
throws OrmException, UnloggedFailure {
List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
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<String> keys, final Account account)
throws OrmException {
ResultSet<AccountSshKey> allKeys = db.accountSshKeys().byAccount(account.getId());
if (keys.contains("ALL")) {
db.accountSshKeys().delete(allKeys);
} else {
List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
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<AccountExternalId> 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<String> readSshKey(final List<String> 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;
}
}