Support adding an SSH key via REST

By POST on /accounts/<account-id>/sshkeys it is now possible to add
an SSH key for a user. The SSH public key has to be provided as raw
content in the request body.

The WebUI is adapted to use the new REST endpint to add SSH keys. The
old RPC for this is deleted.

Change-Id: I9617ccf19f078f9bcee615803755f445ed68bdc9
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2013-05-31 15:11:10 +02:00
committed by Edwin Kempin
parent 1e85b1ec3f
commit 4e2e5216f9
9 changed files with 167 additions and 45 deletions

View File

@@ -509,6 +509,44 @@ describes the SSH key.
}
----
[[add-ssh-key]]
Add SSH Key
~~~~~~~~~~~
[verse]
'POST /accounts/link:#account-id[\{account-id\}]/sshkeys'
Adds an SSH key for a user.
The SSH public key must be provided as raw content in the request body.
.Request
----
POST /accounts/self/sshkeys HTTP/1.0
Content-Type: plain/text
AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d
----
As response an link:#ssh-key-info[SshKeyInfo] entity is returned that
describes the new SSH key.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json;charset=UTF-8
)]}'
{
"seq": 2,
"ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d john.doe@example.com",
"encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d",
"algorithm": "ssh-rsa",
"comment": "john.doe@example.com",
"valid": true
}
----
[[list-account-capabilities]]
List Account Capabilities
~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -31,10 +31,6 @@ import java.util.Set;
@RpcImpl(version = Version.V2_0)
public interface AccountSecurity extends RemoteJsonService {
@Audit
@SignInRequired
void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback);
@Audit
@SignInRequired
void deleteSshKeys(Set<AccountSshKey.Id> ids,

View File

@@ -40,6 +40,13 @@ public class AccountApi {
new RestApi("/accounts/").id(account).view("sshkeys").get(cb);
}
/** Add a new SSH keys */
public static void addSshKey(String account, String sshPublicKey,
AsyncCallback<SshKeyInfo> cb) {
new RestApi("/accounts/").id(account).view("sshkeys")
.post(sshPublicKey, cb);
}
/** Generate a new HTTP password */
public static void generateHttpPassword(String account,
AsyncCallback<NativeString> cb) {

View File

@@ -24,18 +24,6 @@ public class SshKeyInfo extends JavaScriptObject {
public final native String comment() /*-{ return this.comment; }-*/;
public final native boolean isValid() /*-{ return this['valid'] ? true : false; }-*/;
public static native SshKeyInfo create(int seq, String sshPublicKey,
String encodedKey, String algorithm, String comment, boolean valid) /*-{
return {
'seq': seq,
'ssh_public_key': sshPublicKey,
'encoded_key': encodedKey,
'algorithm': algorithm,
'comment': comment,
'valid': valid
};
}-*/;
protected SshKeyInfo() {
}
}

View File

@@ -159,13 +159,11 @@ class SshPanel extends Composite {
final String txt = addTxt.getText();
if (txt != null && txt.length() > 0) {
addNew.setEnabled(false);
Util.ACCOUNT_SEC.addSshKey(txt, new GerritCallback<AccountSshKey>() {
public void onSuccess(final AccountSshKey k) {
AccountApi.addSshKey("self", txt, new GerritCallback<SshKeyInfo>() {
public void onSuccess(final SshKeyInfo k) {
addNew.setEnabled(true);
addTxt.setText("");
keys.addOneKey(SshKeyInfo.create(k.getKey().get(),
k.getSshPublicKey(), k.getEncodedKey(), k.getAlgorithm(),
k.getComment(), k.isValid()));
keys.addOneKey(k);
if (!keys.isVisible()) {
showAddKeyBlock(false);
setKeyTableVisible(true);

View File

@@ -304,6 +304,11 @@ public class RestApi {
sendJSON(POST, content, cb);
}
public <T extends JavaScriptObject> void post(String content,
AsyncCallback<T> cb) {
sendRaw(POST, content, cb);
}
public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
send(PUT, cb);
}
@@ -329,6 +334,19 @@ public class RestApi {
}
}
private <T extends JavaScriptObject> void sendRaw(Method method, String body,
AsyncCallback<T> cb) {
HttpCallback<T> httpCallback = new HttpCallback<T>(cb);
try {
RpcStatus.INSTANCE.onRpcStart();
RequestBuilder req = request(method);
req.setHeader("Content-Type", TEXT_TYPE);
req.sendRequest(body, httpCallback);
} catch (RequestException e) {
httpCallback.onError(null, e);
}
}
private RequestBuilder request(Method method) {
RequestBuilder req = new RequestBuilder(method, url());
if (ifNoneMatch != null) {

View File

@@ -19,7 +19,6 @@ import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.AccountSecurity;
import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.errors.ContactInformationStoreException;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
@@ -106,29 +105,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
this.groupCache = groupCache;
}
public void addSshKey(final String keyText,
final AsyncCallback<AccountSshKey> callback) {
run(callback, new Action<AccountSshKey>() {
public AccountSshKey run(final ReviewDb db) throws OrmException, Failure {
int max = 0;
final Account.Id me = user.get().getAccountId();
for (final AccountSshKey k : db.accountSshKeys().byAccount(me)) {
max = Math.max(max, k.getKey().get());
}
final AccountSshKey key;
try {
key = sshKeyCache.create(new AccountSshKey.Id(me, max + 1), keyText);
} catch (InvalidSshKeyException e) {
throw new Failure(e);
}
db.accountSshKeys().insert(Collections.singleton(key));
uncacheSshKeys();
return key;
}
});
}
public void deleteSshKeys(final Set<AccountSshKey.Id> ids,
final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {

View File

@@ -0,0 +1,100 @@
// Copyright (C) 2013 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.server.account;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import com.google.common.io.InputSupplier;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AddSshKey.Input;
import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
public class AddSshKey implements RestModifyView<AccountResource, Input> {
static class Input {
RawInput raw;
}
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final SshKeyCache sshKeyCache;
@Inject
AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
SshKeyCache sshKeyCache) {
this.self = self;
this.dbProvider = dbProvider;
this.sshKeyCache = sshKeyCache;
}
@Override
public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
throws AuthException, MethodNotAllowedException, BadRequestException,
ResourceConflictException, OrmException, IOException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("not allowed to add SSH keys");
}
if (input == null) {
input = new Input();
}
if (input.raw == null) {
throw new BadRequestException("SSH public key missing");
}
int max = 0;
for (AccountSshKey k : dbProvider.get().accountSshKeys()
.byAccount(rsrc.getUser().getAccountId())) {
max = Math.max(max, k.getKey().get());
}
final InputStream in = input.raw.getInputStream();
String sshPublicKey =
CharStreams.toString(CharStreams.newReaderSupplier(
new InputSupplier<InputStream>() {
@Override
public InputStream getInput() {
return in;
}
}, Charsets.UTF_8));
try {
AccountSshKey sshKey =
sshKeyCache.create(new AccountSshKey.Id(
rsrc.getUser().getAccountId(), max + 1), sshPublicKey);
dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
sshKeyCache.evict(rsrc.getUser().getUserName());
return Response.<SshKeyInfo>created(new SshKeyInfo(sshKey));
} catch (InvalidSshKeyException e) {
throw new BadRequestException(e.getMessage());
}
}
}

View File

@@ -51,6 +51,7 @@ public class Module extends RestApiModule {
put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
post(ACCOUNT_KIND, "sshkeys").to(AddSshKey.class);
get(SSH_KEY_KIND).to(GetSshKey.class);
get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);