Line data Source code
1 : // Copyright (C) 2012 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.sshd.commands;
16 :
17 : import static java.nio.charset.StandardCharsets.UTF_8;
18 : import static java.util.stream.Collectors.toList;
19 :
20 : import com.google.common.base.Strings;
21 : import com.google.gerrit.common.RawInputUtil;
22 : import com.google.gerrit.entities.Account;
23 : import com.google.gerrit.exceptions.EmailException;
24 : import com.google.gerrit.extensions.api.accounts.EmailInput;
25 : import com.google.gerrit.extensions.api.accounts.SshKeyInput;
26 : import com.google.gerrit.extensions.common.EmailInfo;
27 : import com.google.gerrit.extensions.common.HttpPasswordInput;
28 : import com.google.gerrit.extensions.common.Input;
29 : import com.google.gerrit.extensions.common.NameInput;
30 : import com.google.gerrit.extensions.common.SshKeyInfo;
31 : import com.google.gerrit.extensions.restapi.AuthException;
32 : import com.google.gerrit.extensions.restapi.IdString;
33 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
34 : import com.google.gerrit.extensions.restapi.Response;
35 : import com.google.gerrit.extensions.restapi.RestApiException;
36 : import com.google.gerrit.server.CurrentUser;
37 : import com.google.gerrit.server.IdentifiedUser;
38 : import com.google.gerrit.server.account.AccountResource;
39 : import com.google.gerrit.server.account.AccountSshKey;
40 : import com.google.gerrit.server.account.externalids.ExternalIds;
41 : import com.google.gerrit.server.permissions.GlobalPermission;
42 : import com.google.gerrit.server.permissions.PermissionBackend;
43 : import com.google.gerrit.server.permissions.PermissionBackendException;
44 : import com.google.gerrit.server.restapi.account.AddSshKey;
45 : import com.google.gerrit.server.restapi.account.CreateEmail;
46 : import com.google.gerrit.server.restapi.account.DeleteActive;
47 : import com.google.gerrit.server.restapi.account.DeleteEmail;
48 : import com.google.gerrit.server.restapi.account.DeleteExternalIds;
49 : import com.google.gerrit.server.restapi.account.DeleteSshKey;
50 : import com.google.gerrit.server.restapi.account.GetEmails;
51 : import com.google.gerrit.server.restapi.account.GetSshKeys;
52 : import com.google.gerrit.server.restapi.account.PutActive;
53 : import com.google.gerrit.server.restapi.account.PutHttpPassword;
54 : import com.google.gerrit.server.restapi.account.PutName;
55 : import com.google.gerrit.server.restapi.account.PutPreferred;
56 : import com.google.gerrit.sshd.CommandMetaData;
57 : import com.google.gerrit.sshd.SshCommand;
58 : import com.google.inject.Inject;
59 : import com.google.inject.Provider;
60 : import java.io.BufferedReader;
61 : import java.io.IOException;
62 : import java.io.InputStreamReader;
63 : import java.io.UnsupportedEncodingException;
64 : import java.util.ArrayList;
65 : import java.util.Collections;
66 : import java.util.List;
67 : import org.eclipse.jgit.errors.ConfigInvalidException;
68 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
69 : import org.kohsuke.args4j.Argument;
70 : import org.kohsuke.args4j.Option;
71 :
72 : /** Set a user's account settings. * */
73 : @CommandMetaData(name = "set-account", description = "Change an account's settings")
74 1 : final class SetAccountCommand extends SshCommand {
75 :
76 : @Argument(
77 : index = 0,
78 : required = true,
79 : metaVar = "USER",
80 : usage = "full name, email-address, ssh username or account id")
81 : private Account.Id id;
82 :
83 : @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
84 : private String fullName;
85 :
86 : @Option(name = "--active", usage = "set account's state to active")
87 : private boolean active;
88 :
89 : @Option(name = "--inactive", usage = "set account's state to inactive")
90 : private boolean inactive;
91 :
92 1 : @Option(name = "--add-email", metaVar = "EMAIL", usage = "email addresses to add to the account")
93 : private List<String> addEmails = new ArrayList<>();
94 :
95 1 : @Option(
96 : name = "--delete-email",
97 : metaVar = "EMAIL",
98 : usage = "email addresses to delete from the account")
99 : private List<String> deleteEmails = new ArrayList<>();
100 :
101 : @Option(
102 : name = "--preferred-email",
103 : metaVar = "EMAIL",
104 : usage = "a registered email address from the account")
105 : private String preferredEmail;
106 :
107 1 : @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account")
108 : private List<String> addSshKeys = new ArrayList<>();
109 :
110 1 : @Option(
111 : name = "--delete-ssh-key",
112 : metaVar = "-|KEY",
113 : usage = "public keys to delete from the account")
114 : private List<String> deleteSshKeys = new ArrayList<>();
115 :
116 : @Option(
117 : name = "--http-password",
118 : metaVar = "PASSWORD",
119 : usage = "password for HTTP authentication for the account")
120 : private String httpPassword;
121 :
122 : @Option(name = "--clear-http-password", usage = "clear HTTP password for the account")
123 : private boolean clearHttpPassword;
124 :
125 : @Option(name = "--generate-http-password", usage = "generate a new HTTP password for the account")
126 : private boolean generateHttpPassword;
127 :
128 1 : @Option(
129 : name = "--delete-external-id",
130 : metaVar = "EXTERNALID",
131 : usage = "external id to delete from the account")
132 : private List<String> externalIdsToDelete = new ArrayList<>();
133 :
134 : @Inject private IdentifiedUser.GenericFactory genericUserFactory;
135 :
136 : @Inject private CreateEmail createEmail;
137 :
138 : @Inject private DeleteExternalIds deleteExternalIds;
139 :
140 : @Inject private GetEmails getEmails;
141 :
142 : @Inject private DeleteEmail deleteEmail;
143 :
144 : @Inject private PutPreferred putPreferred;
145 :
146 : @Inject private PutName putName;
147 :
148 : @Inject private PutHttpPassword putHttpPassword;
149 :
150 : @Inject private PutActive putActive;
151 :
152 : @Inject private DeleteActive deleteActive;
153 :
154 : @Inject private AddSshKey addSshKey;
155 :
156 : @Inject private GetSshKeys getSshKeys;
157 :
158 : @Inject private DeleteSshKey deleteSshKey;
159 :
160 : @Inject private PermissionBackend permissionBackend;
161 :
162 : @Inject private Provider<CurrentUser> userProvider;
163 :
164 : @Inject private ExternalIds externalIds;
165 :
166 : private AccountResource rsrc;
167 :
168 : @Override
169 : public void run() throws Exception {
170 1 : enableGracefulStop();
171 1 : user = genericUserFactory.create(id);
172 :
173 1 : validate();
174 1 : setAccount();
175 1 : }
176 :
177 : private void validate() throws UnloggedFailure {
178 1 : PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider.get());
179 :
180 1 : boolean isAdmin = userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
181 1 : boolean canModifyAccount =
182 1 : isAdmin || userPermission.testOrFalse(GlobalPermission.MODIFY_ACCOUNT);
183 :
184 1 : if (!user.hasSameAccountId(userProvider.get()) && !canModifyAccount) {
185 1 : throw die(
186 : "Setting another user's account information requries 'modify account' or 'administrate server' capabilities.");
187 : }
188 1 : if (active || inactive) {
189 0 : if (!canModifyAccount) {
190 0 : throw die(
191 : "--active and --inactive require 'modify account' or 'administrate server' capabilities.");
192 : }
193 0 : if (active && inactive) {
194 0 : throw die("--active and --inactive options are mutually exclusive.");
195 : }
196 : }
197 :
198 1 : if (generateHttpPassword && clearHttpPassword) {
199 0 : throw die("--generate-http-password and --clear-http-password are mutually exclusive.");
200 : }
201 1 : if (!Strings.isNullOrEmpty(httpPassword)) { // gave --http-password
202 0 : if (!isAdmin) {
203 0 : throw die("--http-password requires 'administrate server' capabilities.");
204 : }
205 0 : if (generateHttpPassword) {
206 0 : throw die("--http-password and --generate-http-password options are mutually exclusive.");
207 : }
208 0 : if (clearHttpPassword) {
209 0 : throw die("--http-password and --clear-http-password options are mutually exclusive.");
210 : }
211 : }
212 1 : if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
213 0 : throw die("Only one option may use the stdin");
214 : }
215 1 : if (deleteSshKeys.contains("ALL")) {
216 0 : deleteSshKeys = Collections.singletonList("ALL");
217 : }
218 1 : if (deleteEmails.contains("ALL")) {
219 0 : deleteEmails = Collections.singletonList("ALL");
220 : }
221 1 : if (deleteEmails.contains(preferredEmail)) {
222 0 : throw die(
223 : "--preferred-email and --delete-email options are mutually "
224 : + "exclusive for the same email address.");
225 : }
226 1 : if (externalIdsToDelete.contains("ALL")) {
227 1 : externalIdsToDelete = Collections.singletonList("ALL");
228 : }
229 1 : }
230 :
231 : private void setAccount() throws Failure {
232 1 : user = genericUserFactory.create(id);
233 1 : rsrc = new AccountResource(user.asIdentifiedUser());
234 : try {
235 1 : for (String email : addEmails) {
236 0 : addEmail(email);
237 0 : }
238 :
239 1 : for (String email : deleteEmails) {
240 0 : deleteEmail(email);
241 0 : }
242 :
243 1 : if (preferredEmail != null) {
244 0 : putPreferred(preferredEmail);
245 : }
246 :
247 1 : if (fullName != null) {
248 0 : NameInput in = new NameInput();
249 0 : in.name = fullName;
250 0 : putName.apply(rsrc, in);
251 : }
252 :
253 1 : if (httpPassword != null || clearHttpPassword || generateHttpPassword) {
254 0 : HttpPasswordInput in = new HttpPasswordInput();
255 0 : in.httpPassword = httpPassword;
256 0 : if (generateHttpPassword) {
257 0 : in.generate = true;
258 : }
259 0 : Response<String> resp = putHttpPassword.apply(rsrc, in);
260 0 : if (generateHttpPassword) {
261 0 : stdout.print("New password: " + resp.value() + "\n");
262 : }
263 : }
264 :
265 1 : if (active) {
266 0 : putActive.apply(rsrc, null);
267 1 : } else if (inactive) {
268 : try {
269 0 : deleteActive.apply(rsrc, null);
270 0 : } catch (ResourceNotFoundException e) {
271 : // user is already inactive
272 0 : }
273 : }
274 :
275 1 : addSshKeys = readSshKey(addSshKeys);
276 1 : if (!addSshKeys.isEmpty()) {
277 0 : addSshKeys(addSshKeys);
278 : }
279 :
280 1 : deleteSshKeys = readSshKey(deleteSshKeys);
281 1 : if (!deleteSshKeys.isEmpty()) {
282 0 : deleteSshKeys(deleteSshKeys);
283 : }
284 :
285 1 : for (String externalId : externalIdsToDelete) {
286 1 : deleteExternalId(externalId);
287 1 : }
288 1 : } catch (RestApiException e) {
289 1 : throw die(e.getMessage());
290 0 : } catch (Exception e) {
291 0 : throw new Failure(1, "unavailable", e);
292 1 : }
293 1 : }
294 :
295 : private void addSshKeys(List<String> sshKeys)
296 : throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
297 0 : for (String sshKey : sshKeys) {
298 0 : SshKeyInput in = new SshKeyInput();
299 0 : in.raw = RawInputUtil.create(sshKey.getBytes(UTF_8), "text/plain");
300 0 : addSshKey.apply(rsrc, in);
301 0 : }
302 0 : }
303 :
304 : private void deleteSshKeys(List<String> sshKeys) throws Exception {
305 0 : List<SshKeyInfo> infos = getSshKeys.apply(rsrc).value();
306 0 : if (sshKeys.contains("ALL")) {
307 0 : for (SshKeyInfo i : infos) {
308 0 : deleteSshKey(i);
309 0 : }
310 : } else {
311 0 : for (String sshKey : sshKeys) {
312 0 : for (SshKeyInfo i : infos) {
313 0 : if (sshKey.trim().equals(i.sshPublicKey) || sshKey.trim().equals(i.comment)) {
314 0 : deleteSshKey(i);
315 : }
316 0 : }
317 0 : }
318 : }
319 0 : }
320 :
321 : private void deleteSshKey(SshKeyInfo i)
322 : throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
323 : PermissionBackendException {
324 0 : AccountSshKey sshKey = AccountSshKey.create(user.getAccountId(), i.seq, i.sshPublicKey);
325 0 : deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
326 0 : }
327 :
328 : private void addEmail(String email)
329 : throws UnloggedFailure, RestApiException, IOException, ConfigInvalidException,
330 : PermissionBackendException {
331 0 : EmailInput in = new EmailInput();
332 0 : in.email = email;
333 0 : in.noConfirmation = true;
334 : try {
335 0 : createEmail.apply(rsrc, IdString.fromDecoded(email), in);
336 0 : } catch (EmailException e) {
337 0 : throw die(e.getMessage());
338 0 : }
339 0 : }
340 :
341 : private void deleteEmail(String email) throws Exception {
342 0 : if (email.equals("ALL")) {
343 0 : List<EmailInfo> emails = getEmails.apply(rsrc).value();
344 0 : for (EmailInfo e : emails) {
345 0 : deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), e.email), new Input());
346 0 : }
347 0 : } else {
348 0 : deleteEmail.apply(new AccountResource.Email(user.asIdentifiedUser(), email), new Input());
349 : }
350 0 : }
351 :
352 : private void putPreferred(String email) throws Exception {
353 0 : for (EmailInfo e : getEmails.apply(rsrc).value()) {
354 0 : if (e.email.equals(email)) {
355 0 : putPreferred.apply(new AccountResource.Email(user.asIdentifiedUser(), email), null);
356 0 : return;
357 : }
358 0 : }
359 0 : stderr.println("preferred email not found: " + email);
360 0 : }
361 :
362 : private List<String> readSshKey(List<String> sshKeys)
363 : throws UnsupportedEncodingException, IOException {
364 1 : if (!sshKeys.isEmpty()) {
365 0 : int idx = sshKeys.indexOf("-");
366 0 : if (idx >= 0) {
367 0 : StringBuilder sshKey = new StringBuilder();
368 0 : BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8));
369 : String line;
370 0 : while ((line = br.readLine()) != null) {
371 0 : sshKey.append(line).append("\n");
372 : }
373 0 : sshKeys.set(idx, sshKey.toString());
374 : }
375 : }
376 1 : return sshKeys;
377 : }
378 :
379 : private void deleteExternalId(String externalId)
380 : throws IOException, RestApiException, ConfigInvalidException, PermissionBackendException {
381 : List<String> ids;
382 1 : if (externalId.equals("ALL")) {
383 1 : ids =
384 1 : externalIds.byAccount(rsrc.getUser().getAccountId()).stream()
385 1 : .map(e -> e.key().get())
386 1 : .collect(toList());
387 1 : if (ids.isEmpty()) {
388 0 : throw new ResourceNotFoundException("Account has no external Ids");
389 : }
390 : } else {
391 1 : ids = Collections.singletonList(externalId);
392 : }
393 1 : deleteExternalIds.apply(rsrc, ids);
394 1 : }
395 : }
|