diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt new file mode 100644 index 0000000000..6936e3ffc9 --- /dev/null +++ b/Documentation/cmd-create-account.txt @@ -0,0 +1,70 @@ +gerrit create-account +===================== + +NAME +---- +gerrit create-account - Create a new batch/role account. + +SYNOPSIS +-------- +[verse] +'ssh' -p 'gerrit create-account' \ +[\--group ] \ +[\--full-name ] \ +[\--email ] \ +[\--ssh-key -|] \ + + +DESCRIPTION +----------- +Creates a new internal only user account for batch/role access, such +as from an automated build system or event monitoring over +link:cmd-stream-events.html[gerrit stream-events]. + +If LDAP authentication is being used, the user account is created +without checking the LDAP directory. Consequently users can be +created in Gerrit that do not exist in the underlying LDAP directory. + +ACCESS +------ +Caller must be a member of the privileged 'Administrators' group. + +SCRIPTING +--------- +This command is intended to be used in scripts. + +OPTIONS +------- +:: + Required; SSH username of the user account. + +\--ssh-key:: + Content of the public SSH key to load into the account's + keyring. If `-` the key is read from stdin, rather than + from the command line. + +\--group:: + Name of the group to put the user into. Multiple \--group + options may be specified to add the user to multiple groups. + +\--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\'"`. + +\--email:: + Preferred email address for the user account. + +EXAMPLES +-------- +Create a new user account called `watcher`: + +==== + $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-user --ssh-key - watcher +==== + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt index bad8f7055d..bc08ae94b8 100644 --- a/Documentation/cmd-index.txt +++ b/Documentation/cmd-index.txt @@ -75,6 +75,9 @@ gerrit receive-pack:: [[admin_commands]]Adminstrator Commands ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +link:cmd-create-account.html[gerrit create-account]:: + Create a new batch/role account. + link:cmd-create-project.html[gerrit create-project]:: Create a new project and associated Git repository. diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java new file mode 100644 index 0000000000..48194f9902 --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java @@ -0,0 +1,179 @@ +// Copyright (C) 2010 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.Account; +import com.google.gerrit.reviewdb.AccountExternalId; +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.AccountGroupMember; +import com.google.gerrit.reviewdb.AccountGroupMemberAudit; +import com.google.gerrit.reviewdb.AccountSshKey; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountByEmailCache; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.ssh.SshKeyCache; +import com.google.gerrit.sshd.AdminCommand; +import com.google.gerrit.sshd.BaseCommand; +import com.google.gwtorm.client.OrmDuplicateKeyException; +import com.google.gwtorm.client.OrmException; +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.HashSet; +import java.util.List; + +/** Create a new user account. **/ +@AdminCommand +final class AdminCreateAccount extends BaseCommand { + @Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to") + private List groups = new ArrayList(); + + @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account") + private String fullName; + + @Option(name = "--email", metaVar = "EMAIL", usage = "email address of the account") + private String email; + + @Option(name = "--ssh-key", metaVar = "-|KEY", usage = "public key for SSH authentication") + private String sshKey; + + @Argument(index = 0, required = true, metaVar = "USERNAME", usage = "name of the user account") + private String username; + + @Inject + private IdentifiedUser currentUser; + + @Inject + private ReviewDb db; + + @Inject + private SshKeyCache sshKeyCache; + + @Inject + private AccountCache accountCache; + + @Inject + private AccountByEmailCache byEmailCache; + + @Override + public void start(final Environment env) { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(); + createAccount(); + } + }); + } + + private void createAccount() throws OrmException, IOException, + InvalidSshKeyException, UnloggedFailure { + if (!username.matches(Account.USER_NAME_PATTERN)) { + throw die("Username '" + username + "'" + + " must contain only letters, numbers, _, - or ."); + } + + final Account.Id id = new Account.Id(db.nextAccountId()); + final AccountSshKey key = readSshKey(id); + + AccountExternalId extUser = + new AccountExternalId(id, new AccountExternalId.Key( + AccountExternalId.SCHEME_USERNAME, username)); + + if (db.accountExternalIds().get(extUser.getKey()) != null) { + throw die("username '" + username + "' already exists"); + } + if (email != null && db.accountExternalIds().get(getEmailKey()) != null) { + throw die("email '" + email + "' already exists"); + } + + try { + db.accountExternalIds().insert(Collections.singleton(extUser)); + } catch (OrmDuplicateKeyException duplicateKey) { + throw die("username '" + username + "' already exists"); + } + + if (email != null) { + AccountExternalId extMailto = new AccountExternalId(id, getEmailKey()); + extMailto.setEmailAddress(email); + try { + db.accountExternalIds().insert(Collections.singleton(extMailto)); + } catch (OrmDuplicateKeyException duplicateKey) { + try { + db.accountExternalIds().delete(Collections.singleton(extUser)); + } catch (OrmException cleanupError) { + } + throw die("email '" + email + "' already exists"); + } + } + + Account a = new Account(id); + a.setFullName(fullName); + a.setPreferredEmail(email); + db.accounts().insert(Collections.singleton(a)); + + if (key != null) { + db.accountSshKeys().insert(Collections.singleton(key)); + } + + for (AccountGroup.Id groupId : new HashSet(groups)) { + AccountGroupMember m = + new AccountGroupMember(new AccountGroupMember.Key(id, groupId)); + db.accountGroupMembersAudit().insert(Collections.singleton( // + new AccountGroupMemberAudit(m, currentUser.getAccountId()))); + db.accountGroupMembers().insert(Collections.singleton(m)); + } + + sshKeyCache.evict(username); + accountCache.evictByUsername(username); + byEmailCache.evict(email); + } + + private UnloggedFailure die(String msg) { + return new UnloggedFailure(1, "fatal: " + msg); + } + + private AccountExternalId.Key getEmailKey() { + return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email); + } + + private AccountSshKey readSshKey(final Account.Id id) + throws UnsupportedEncodingException, IOException, InvalidSshKeyException { + if (sshKey == null) { + return null; + } + if ("-".equals(sshKey)) { + sshKey = ""; + BufferedReader br = + new BufferedReader(new InputStreamReader(in, "UTF-8")); + String line; + while ((line = br.readLine()) != null) { + sshKey += line + "\n"; + } + } + return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim()); + } +} 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 1319671822..b8d7dc0729 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 @@ -26,6 +26,7 @@ public class MasterCommandModule extends CommandModule { final CommandName gerrit = Commands.named("gerrit"); command(gerrit, "approve").to(ApproveCommand.class); + command(gerrit, "create-account").to(AdminCreateAccount.class); command(gerrit, "create-project").to(AdminCreateProject.class); command(gerrit, "gsql").to(AdminQueryShell.class); command(gerrit, "receive-pack").to(Receive.class); diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java index c32592869d..0dfd4ef0e3 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java @@ -26,6 +26,7 @@ public class SlaveCommandModule extends CommandModule { final CommandName gerrit = Commands.named("gerrit"); command(gerrit, "approve").to(ErrorSlaveMode.class); + command(gerrit, "create-account").to(ErrorSlaveMode.class); command(gerrit, "create-project").to(ErrorSlaveMode.class); command(gerrit, "gsql").to(ErrorSlaveMode.class); command(gerrit, "receive-pack").to(ErrorSlaveMode.class);