| // 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 static java.nio.charset.StandardCharsets.UTF_8; | |
| import static java.util.stream.Collectors.toList; | |
| import com.google.common.base.Strings; | |
| import com.google.gerrit.common.RawInputUtil; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.exceptions.EmailException; | |
| import com.google.gerrit.extensions.api.accounts.EmailInput; | |
| import com.google.gerrit.extensions.api.accounts.SshKeyInput; | |
| import com.google.gerrit.extensions.common.EmailInfo; | |
| import com.google.gerrit.extensions.common.HttpPasswordInput; | |
| import com.google.gerrit.extensions.common.Input; | |
| import com.google.gerrit.extensions.common.NameInput; | |
| import com.google.gerrit.extensions.common.SshKeyInfo; | |
| import com.google.gerrit.extensions.restapi.AuthException; | |
| import com.google.gerrit.extensions.restapi.IdString; | |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; | |
| import com.google.gerrit.extensions.restapi.Response; | |
| import com.google.gerrit.extensions.restapi.RestApiException; | |
| import com.google.gerrit.server.CurrentUser; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.account.AccountResource; | |
| import com.google.gerrit.server.account.AccountSshKey; | |
| import com.google.gerrit.server.account.externalids.ExternalIds; | |
| import com.google.gerrit.server.permissions.GlobalPermission; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.permissions.PermissionBackendException; | |
| import com.google.gerrit.server.restapi.account.AddSshKey; | |
| import com.google.gerrit.server.restapi.account.CreateEmail; | |
| import com.google.gerrit.server.restapi.account.DeleteActive; | |
| import com.google.gerrit.server.restapi.account.DeleteEmail; | |
| import com.google.gerrit.server.restapi.account.DeleteExternalIds; | |
| import com.google.gerrit.server.restapi.account.DeleteSshKey; | |
| import com.google.gerrit.server.restapi.account.GetEmails; | |
| import com.google.gerrit.server.restapi.account.GetSshKeys; | |
| import com.google.gerrit.server.restapi.account.PutActive; | |
| import com.google.gerrit.server.restapi.account.PutHttpPassword; | |
| import com.google.gerrit.server.restapi.account.PutName; | |
| import com.google.gerrit.server.restapi.account.PutPreferred; | |
| import com.google.gerrit.sshd.CommandMetaData; | |
| import com.google.gerrit.sshd.SshCommand; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| 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; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; | |
| import org.kohsuke.args4j.Argument; | |
| import org.kohsuke.args4j.Option; | |
| /** Set a user's account settings. * */ | |
| @CommandMetaData(name = "set-account", description = "Change an account's settings") | |
| final class SetAccountCommand extends SshCommand { | |
| @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", metaVar = "EMAIL", usage = "email addresses to add to the account") | |
| private List<String> addEmails = new ArrayList<>(); | |
| @Option( | |
| name = "--delete-email", | |
| metaVar = "EMAIL", | |
| usage = "email addresses to delete from the account") | |
| private List<String> deleteEmails = new ArrayList<>(); | |
| @Option( | |
| name = "--preferred-email", | |
| metaVar = "EMAIL", | |
| usage = "a registered email address from the account") | |
| private String preferredEmail; | |
| @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account") | |
| private List<String> addSshKeys = new ArrayList<>(); | |
| @Option( | |
| name = "--delete-ssh-key", | |
| metaVar = "-|KEY", | |
| usage = "public keys to delete from the account") | |
| private List<String> deleteSshKeys = new ArrayList<>(); | |
| @Option( | |
| name = "--http-password", | |
| metaVar = "PASSWORD", | |
| usage = "password for HTTP authentication for the account") | |
| private String httpPassword; | |
| @Option(name = "--clear-http-password", usage = "clear HTTP password for the account") | |
| private boolean clearHttpPassword; | |
| @Option(name = "--generate-http-password", usage = "generate a new HTTP password for the account") | |
| private boolean generateHttpPassword; | |
| @Option( | |
| name = "--delete-external-id", | |
| metaVar = "EXTERNALID", | |
| usage = "external id to delete from the account") | |
| private List<String> externalIdsToDelete = new ArrayList<>(); | |
| @Inject private IdentifiedUser.GenericFactory genericUserFactory; | |
| @Inject private CreateEmail createEmail; | |
| @Inject private DeleteExternalIds deleteExternalIds; | |
| @Inject private GetEmails getEmails; | |
| @Inject private DeleteEmail deleteEmail; | |
| @Inject private PutPreferred putPreferred; | |
| @Inject private PutName putName; | |
| @Inject private PutHttpPassword putHttpPassword; | |
| @Inject private PutActive putActive; | |
| @Inject private DeleteActive deleteActive; | |
| @Inject private AddSshKey addSshKey; | |
| @Inject private GetSshKeys getSshKeys; | |
| @Inject private DeleteSshKey deleteSshKey; | |
| @Inject private PermissionBackend permissionBackend; | |
| @Inject private Provider<CurrentUser> userProvider; | |
| @Inject private ExternalIds externalIds; | |
| private AccountResource rsrc; | |
| @Override | |
| public void run() throws Exception { | |
| enableGracefulStop(); | |
| user = genericUserFactory.create(id); | |
| validate(); | |
| setAccount(); | |
| } | |
| private void validate() throws UnloggedFailure { | |
| PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider.get()); | |
| boolean isAdmin = userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER); | |
| boolean canModifyAccount = | |
| isAdmin || userPermission.testOrFalse(GlobalPermission.MODIFY_ACCOUNT); | |
| if (!user.hasSameAccountId(userProvider.get()) && !canModifyAccount) { | |
| throw die( | |
| "Setting another user's account information requries 'modify account' or 'administrate server' capabilities."); | |
| } | |
| if (active || inactive) { | |
| if (!canModifyAccount) { | |
| throw die( | |
| "--active and --inactive require 'modify account' or 'administrate server' capabilities."); | |
| } | |
| if (active && inactive) { | |
| throw die("--active and --inactive options are mutually exclusive."); | |
| } | |
| } | |
| if (generateHttpPassword && clearHttpPassword) { | |
| throw die("--generate-http-password and --clear-http-password are mutually exclusive."); | |
| } | |
| if (!Strings.isNullOrEmpty(httpPassword)) { // gave --http-password | |
| if (!isAdmin) { | |
| throw die("--http-password requires 'administrate server' capabilities."); | |
| } | |
| if (generateHttpPassword) { | |
| throw die("--http-password and --generate-http-password options are mutually exclusive."); | |
| } | |
| if (clearHttpPassword) { | |
| throw die("--http-password and --clear-http-password options are mutually exclusive."); | |
| } | |
| } | |
| if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) { | |
| throw die("Only one option may use the stdin"); | |
| } | |
| if (deleteSshKeys.contains("ALL")) { | |
| deleteSshKeys = Collections.singletonList("ALL"); | |
| } | |
| if (deleteEmails.contains("ALL")) { | |
| deleteEmails = Collections.singletonList("ALL"); | |
| } | |
| if (deleteEmails.contains(preferredEmail)) { | |
| throw die( | |
| "--preferred-email and --delete-email options are mutually " | |
| + "exclusive for the same email address."); | |
| } | |
| if (externalIdsToDelete.contains("ALL")) { | |
| externalIdsToDelete = Collections.singletonList("ALL"); | |
| } | |
| } | |
| private void setAccount() throws Failure { | |
| user = genericUserFactory.create(id); | |
| rsrc = new AccountResource(user.asIdentifiedUser()); | |
| try { | |
| for (String email : addEmails) { | |
| addEmail(email); | |
| } | |
| for (String email : deleteEmails) { | |
| deleteEmail(email); | |
| } | |
| if (preferredEmail != null) { | |
| putPreferred(preferredEmail); | |
| } | |
| if (fullName != null) { | |
| NameInput in = new NameInput(); | |
| in.name = fullName; | |
| putName.apply(rsrc, in); | |
| } | |
| if (httpPassword != null || clearHttpPassword || generateHttpPassword) { | |
| HttpPasswordInput in = new HttpPasswordInput(); | |
| in.httpPassword = httpPassword; | |
| if (generateHttpPassword) { | |
| in.generate = true; | |
| } | |
| Response<String> resp = putHttpPassword.apply(rsrc, in); | |
| if (generateHttpPassword) { | |
| stdout.print("New password: " + resp.value() + "\n"); | |
| } | |
| } | |
| if (active) { | |
| putActive.apply(rsrc, null); | |
| } else if (inactive) { | |
| try { | |
| deleteActive.apply(rsrc, null); | |
| } catch (ResourceNotFoundException e) { | |
| // user is already inactive | |
| } | |
| } | |
| addSshKeys = readSshKey(addSshKeys); | |
| if (!addSshKeys.isEmpty()) { | |
| addSshKeys(addSshKeys); | |
| } | |
| deleteSshKeys = readSshKey(deleteSshKeys); | |
| if (!deleteSshKeys.isEmpty()) { | |
| deleteSshKeys(deleteSshKeys); | |
| } | |
| for (String externalId : externalIdsToDelete) { | |
| deleteExternalId(externalId); | |
| } | |
| } catch (RestApiException e) { | |
| throw die(e.getMessage()); | |
| } catch (Exception e) { | |
| throw new Failure(1, "unavailable", e); | |
| } | |
| } | |
| private void addSshKeys(List<String> sshKeys) | |
| throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException { | |
| for (String sshKey : sshKeys) { | |
| SshKeyInput in = new SshKeyInput(); | |
| in.raw = RawInputUtil.create(sshKey.getBytes(UTF_8), "text/plain"); | |
| addSshKey.apply(rsrc, in); | |
| } | |
| } | |
| private void deleteSshKeys(List<String> sshKeys) throws Exception { | |
| List<SshKeyInfo> infos = getSshKeys.apply(rsrc).value(); | |
| if (sshKeys.contains("ALL")) { | |
| for (SshKeyInfo i : infos) { | |
| deleteSshKey(i); | |
| } | |
| } else { | |
| for (String sshKey : sshKeys) { | |
| for (SshKeyInfo i : infos) { | |
| if (sshKey.trim().equals(i.sshPublicKey) || sshKey.trim().equals(i.comment)) { | |
| deleteSshKey(i); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private void deleteSshKey(SshKeyInfo i) | |
| throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException, | |
| PermissionBackendException { | |
| AccountSshKey sshKey = AccountSshKey.create(user.getAccountId(), i.seq, i.sshPublicKey); | |
| deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null); | |
| } | |
| private void addEmail(String email) | |
| throws UnloggedFailure, RestApiException, IOException, ConfigInvalidException, | |
| PermissionBackendException { | |
| EmailInput in = new EmailInput(); | |
| in.email = email; | |
| in.noConfirmation = true; | |
| try { | |
| createEmail.apply(rsrc, IdString.fromDecoded(email), in); | |
| } catch (EmailException e) { | |
| throw die(e.getMessage()); | |
| } | |
| } | |
| private void deleteEmail(String email) throws Exception { | |
| if (email.equals("ALL")) { | |
| List<EmailInfo> emails = getEmails.apply(rsrc).value(); | |
| for (EmailInfo e : emails) { | |
| deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), e.email), new Input()); | |
| } | |
| } else { | |
| deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), email), new Input()); | |
| } | |
| } | |
| private void putPreferred(String email) throws Exception { | |
| for (EmailInfo e : getEmails.apply(rsrc).value()) { | |
| if (e.email.equals(email)) { | |
| putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null); | |
| return; | |
| } | |
| } | |
| stderr.println("preferred email not found: " + email); | |
| } | |
| private List<String> readSshKey(List<String> sshKeys) | |
| throws UnsupportedEncodingException, IOException { | |
| if (!sshKeys.isEmpty()) { | |
| int idx = sshKeys.indexOf("-"); | |
| if (idx >= 0) { | |
| StringBuilder sshKey = new StringBuilder(); | |
| BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8)); | |
| String line; | |
| while ((line = br.readLine()) != null) { | |
| sshKey.append(line).append("\n"); | |
| } | |
| sshKeys.set(idx, sshKey.toString()); | |
| } | |
| } | |
| return sshKeys; | |
| } | |
| private void deleteExternalId(String externalId) | |
| throws IOException, RestApiException, ConfigInvalidException, PermissionBackendException { | |
| List<String> ids; | |
| if (externalId.equals("ALL")) { | |
| ids = | |
| externalIds.byAccount(rsrc.getUser().getAccountId()).stream() | |
| .map(e -> e.key().get()) | |
| .collect(toList()); | |
| if (ids.isEmpty()) { | |
| throw new ResourceNotFoundException("Account has no external Ids"); | |
| } | |
| } else { | |
| ids = Collections.singletonList(externalId); | |
| } | |
| deleteExternalIds.apply(rsrc, ids); | |
| } | |
| } |