Merge "Support adding an SSH key via REST"

This commit is contained in:
Shawn Pearce
2013-06-04 09:39:53 +00:00
committed by Gerrit Code Review
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);