diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index 9850c40f25..af93dd74a8 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt @@ -222,6 +222,85 @@ Sets the account state to inactive. If the account was already inactive the response is `404 Not Found`. +[[get-http-password]] +Get HTTP Password +~~~~~~~~~~~~~~~~~ +[verse] +'GET /accounts/link:#account-id[\{account-id\}]/password.http' + +Retrieves the HTTP password of an account. + +.Request +---- + GET /accounts/john.doe@example.com/password.http HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json;charset=UTF-8 + + )]}' + "ETxgpih8xrNs" +---- + +If the account does not have an HTTP password the response is `404 Not Found`. + +[[set-http-password]] +Set/Generate HTTP Password +~~~~~~~~~~~~~~~~~~~~~~~~~~ +[verse] +'PUT /accounts/link:#account-id[\{account-id\}]/password.http' + +Sets/Generates the HTTP password of an account. + +The options for setting/generating the HTTP password must be provided +in the request body inside a link:#http-password-input[ +HttpPasswordInput] entity. + +.Request +---- + PUT /accounts/self/password.http HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "generate": true + } +---- + +As response the new HTTP password is returned. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json;charset=UTF-8 + + )]}' + "ETxgpih8xrNs" +---- + +If the HTTP password was deleted the response is "`204 No Content`". + +[[delete-http-password]] +Delete HTTP Password +~~~~~~~~~~~~~~~~~~~~ +[verse] +'DELETE /accounts/link:#account-id[\{account-id\}]/password.http' + +Deletes the HTTP password of an account. + +.Request +---- + DELETE /accounts/self/password.http HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + [[list-account-emails]] List Account Emails ~~~~~~~~~~~~~~~~~~~ @@ -965,6 +1044,24 @@ Only Gerrit administrators are allowed to add email addresses without confirmation. |============================== +[[http-password-input]] +HttpPasswordInput +~~~~~~~~~~~~~~~~~ +The `HttpPasswordInput` entity contains information for setting/generating +an HTTP password. + +[options="header",width="50%",cols="1,^1,5"] +|============================ +|Field Name ||Description +|`generate` |`false` if not set| +Whether a new HTTP password should be generated +|`http_password`|optional| +The new HTTP password. Only Gerrit administrators may set the HTTP +password directly. + +If empty or not set and `generate` is false or not set, the HTTP +password is deleted. +|============================ + [[query-limit-info]] QueryLimitInfo ~~~~~~~~~~~~~~ diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java index 46ea9bacf8..48c269d55f 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java @@ -47,16 +47,6 @@ public interface AccountSecurity extends RemoteJsonService { @SignInRequired void changeUserName(String newName, AsyncCallback callback); - @Audit - @SignInRequired - void generatePassword(AccountExternalId.Key key, - AsyncCallback callback); - - @Audit - @SignInRequired - void clearPassword(AccountExternalId.Key key, - AsyncCallback gerritCallback); - @SignInRequired void myExternalIds(AsyncCallback> callback); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java index 70aba3d156..226216647c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java @@ -14,6 +14,7 @@ package com.google.gerrit.client.account; +import com.google.gerrit.client.VoidResult; import com.google.gerrit.client.rpc.NativeString; import com.google.gerrit.client.rpc.RestApi; import com.google.gwt.core.client.JavaScriptObject; @@ -31,4 +32,29 @@ public class AccountApi { new RestApi("/accounts/").id(account).view("emails").id(email) .ifNoneMatch().put(in, cb); } + + /** Generate a new HTTP password */ + public static void generateHttpPassword(String account, + AsyncCallback cb) { + HttpPasswordInput in = HttpPasswordInput.create(); + in.generate(true); + new RestApi("/accounts/").id(account).view("password.http").put(in, cb); + } + + /** Clear HTTP password */ + public static void clearHttpPassword(String account, + AsyncCallback cb) { + new RestApi("/accounts/").id(account).view("password.http").delete(cb); + } + + private static class HttpPasswordInput extends JavaScriptObject { + final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/; + + static HttpPasswordInput create() { + return createObject().cast(); + } + + protected HttpPasswordInput() { + } + } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java index abb1f4d1cf..3c3b57323b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java @@ -17,7 +17,9 @@ package com.google.gerrit.client.account; import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.VoidResult; import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.rpc.NativeString; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gwt.event.dom.client.ClickEvent; @@ -152,10 +154,12 @@ public class MyPasswordScreen extends SettingsScreen { private void doGeneratePassword() { if (id != null) { enableUI(false); - Util.ACCOUNT_SEC.generatePassword(id.getKey(), - new GerritCallback() { - public void onSuccess(final AccountExternalId result) { - display(result); + AccountApi.generateHttpPassword("self", + new GerritCallback() { + @Override + public void onSuccess(NativeString newPassword) { + id.setPassword(newPassword.asString()); + display(id); } @Override @@ -169,10 +173,12 @@ public class MyPasswordScreen extends SettingsScreen { private void doClearPassword() { if (id != null) { enableUI(false); - Util.ACCOUNT_SEC.clearPassword(id.getKey(), - new GerritCallback() { - public void onSuccess(final AccountExternalId result) { - display(result); + AccountApi.clearHttpPassword("self", + new GerritCallback() { + @Override + public void onSuccess(VoidResult result) { + id.setPassword(null); + display(id); } @Override diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java index f7ba6df7aa..7878477af2 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java @@ -29,8 +29,6 @@ import com.google.gerrit.httpd.rpc.UiRpcModule; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.server.CmdLineParserModule; import com.google.gerrit.server.RemotePeer; -import com.google.gerrit.server.account.ClearPassword; -import com.google.gerrit.server.account.GeneratePassword; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.FactoryModule; @@ -130,10 +128,8 @@ public class WebModule extends FactoryModule { bind(GerritConfig.class).toProvider(GerritConfigProvider.class); DynamicSet.setOf(binder(), WebUiPlugin.class); - factory(ClearPassword.Factory.class); install(new AsyncReceiveCommits.Module()); install(new CmdLineParserModule()); - factory(GeneratePassword.Factory.class); bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider( HttpRemotePeerProvider.class).in(RequestScoped.class); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java index de1295c08f..ec214362d2 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java @@ -39,8 +39,6 @@ import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.ChangeUserName; -import com.google.gerrit.server.account.ClearPassword; -import com.google.gerrit.server.account.GeneratePassword; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.Realm; import com.google.gerrit.server.contact.ContactStore; @@ -70,8 +68,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements private final AccountManager accountManager; private final boolean useContactInfo; - private final ClearPassword.Factory clearPasswordFactory; - private final GeneratePassword.Factory generatePasswordFactory; private final ChangeUserName.CurrentUser changeUserNameFactory; private final DeleteExternalIds.Factory deleteExternalIdsFactory; private final ExternalIdDetailFactory.Factory externalIdDetailFactory; @@ -86,8 +82,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements final EmailTokenVerifier etv, final ProjectCache pc, final SshKeyCache skc, final AccountByEmailCache abec, final AccountCache uac, final AccountManager am, - final ClearPassword.Factory clearPasswordFactory, - final GeneratePassword.Factory generatePasswordFactory, final ChangeUserName.CurrentUser changeUserNameFactory, final DeleteExternalIds.Factory deleteExternalIdsFactory, final ExternalIdDetailFactory.Factory externalIdDetailFactory, @@ -105,8 +99,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements useContactInfo = contactStore != null && contactStore.isEnabled(); - this.clearPasswordFactory = clearPasswordFactory; - this.generatePasswordFactory = generatePasswordFactory; this.changeUserNameFactory = changeUserNameFactory; this.deleteExternalIdsFactory = deleteExternalIdsFactory; this.externalIdDetailFactory = externalIdDetailFactory; @@ -179,18 +171,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements } } - @Override - public void generatePassword(AccountExternalId.Key key, - AsyncCallback callback) { - Handler.wrap(generatePasswordFactory.create(key)).to(callback); - } - - @Override - public void clearPassword(AccountExternalId.Key key, - AsyncCallback callback) { - Handler.wrap(clearPasswordFactory.create(key)).to(callback); - } - public void myExternalIds(AsyncCallback> callback) { externalIdDetailFactory.create().to(callback); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java deleted file mode 100644 index 255c248f88..0000000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java +++ /dev/null @@ -1,63 +0,0 @@ -// 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.server.account; - -import com.google.gerrit.common.errors.NoSuchEntityException; -import com.google.gerrit.reviewdb.client.AccountExternalId; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -import java.util.Collections; -import java.util.concurrent.Callable; - -/** Operation to clear a password for an account. */ -public class ClearPassword implements Callable { - public interface Factory { - ClearPassword create(AccountExternalId.Key forUser); - } - - private final AccountCache accountCache; - private final ReviewDb db; - private final IdentifiedUser user; - - private final AccountExternalId.Key forUser; - - @Inject - ClearPassword(final AccountCache accountCache, final ReviewDb db, - final IdentifiedUser user, - - @Assisted AccountExternalId.Key forUser) { - this.accountCache = accountCache; - this.db = db; - this.user = user; - - this.forUser = forUser; - } - - public AccountExternalId call() throws OrmException, NoSuchEntityException { - AccountExternalId id = db.accountExternalIds().get(forUser); - if (id == null || !user.getAccountId().equals(id.getAccountId())) { - throw new NoSuchEntityException(); - } - - id.setPassword(null); - db.accountExternalIds().update(Collections.singleton(id)); - accountCache.evict(user.getAccountId()); - return id; - } -} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java deleted file mode 100644 index bbab126de2..0000000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java +++ /dev/null @@ -1,93 +0,0 @@ -// 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.server.account; - -import com.google.gerrit.common.errors.NoSuchEntityException; -import com.google.gerrit.reviewdb.client.AccountExternalId; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -import org.apache.commons.codec.binary.Base64; - -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Collections; -import java.util.concurrent.Callable; - -/** Operation to generate a password for an account. */ -public class GeneratePassword implements Callable { - private static final int LEN = 12; - private static final SecureRandom rng; - - static { - try { - rng = SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Cannot create RNG for password generator", e); - } - } - - public interface Factory { - GeneratePassword create(AccountExternalId.Key forUser); - } - - private final AccountCache accountCache; - private final ReviewDb db; - private final IdentifiedUser user; - - private final AccountExternalId.Key forUser; - - @Inject - GeneratePassword(final AccountCache accountCache, final ReviewDb db, - final IdentifiedUser user, - - @Assisted AccountExternalId.Key forUser) { - this.accountCache = accountCache; - this.db = db; - this.user = user; - - this.forUser = forUser; - } - - public AccountExternalId call() throws OrmException, NoSuchEntityException { - AccountExternalId id = db.accountExternalIds().get(forUser); - if (id == null || !user.getAccountId().equals(id.getAccountId())) { - throw new NoSuchEntityException(); - } - - id.setPassword(generate()); - db.accountExternalIds().update(Collections.singleton(id)); - accountCache.evict(user.getAccountId()); - return id; - } - - private String generate() { - byte[] rand = new byte[LEN]; - rng.nextBytes(rand); - - byte[] enc = Base64.encodeBase64(rand, false); - StringBuilder r = new StringBuilder(LEN); - for (int i = 0; i < LEN; i++) { - if (enc[i] == '=') { - break; - } - r.append((char) enc[i]); - } - return r.toString(); - } -} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java new file mode 100644 index 0000000000..8eaf4b3b49 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java @@ -0,0 +1,51 @@ +// 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.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.server.CurrentUser; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class GetHttpPassword implements RestReadView { + + private final Provider self; + + @Inject + GetHttpPassword(Provider self) { + this.self = self; + } + + @Override + public String apply(AccountResource rsrc) throws AuthException, + ResourceNotFoundException, OrmException { + if (self.get() != rsrc.getUser() + && !self.get().getCapabilities().canAdministrateServer()) { + throw new AuthException("not allowed to get http password"); + } + AccountState s = rsrc.getUser().state(); + if (s.getUserName() == null) { + throw new ResourceNotFoundException(); + } + String p = s.getPassword(s.getUserName()); + if (p == null) { + throw new ResourceNotFoundException(); + } + return p; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java index 335ac79065..a21b550379 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java @@ -45,6 +45,9 @@ public class Module extends RestApiModule { put(EMAIL_KIND).to(PutEmail.class); delete(EMAIL_KIND).to(DeleteEmail.class); put(EMAIL_KIND, "preferred").to(PutPreferred.class); + get(ACCOUNT_KIND, "password.http").to(GetHttpPassword.class); + put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class); + delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class); get(ACCOUNT_KIND, "avatar").to(GetAvatar.class); get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class); child(ACCOUNT_KIND, "capabilities").to(Capabilities.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java new file mode 100644 index 0000000000..02e0866630 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java @@ -0,0 +1,123 @@ +// 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 static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.reviewdb.client.AccountExternalId; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.PutHttpPassword.Input; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.apache.commons.codec.binary.Base64; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; + +public class PutHttpPassword implements RestModifyView { + static class Input { + String httpPassword; + boolean generate; + } + + private static final int LEN = 12; + private static final SecureRandom rng; + + static { + try { + rng = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Cannot create RNG for password generator", e); + } + } + + private final Provider self; + private final Provider dbProvider; + private final AccountCache accountCache; + + @Inject + PutHttpPassword(Provider self, Provider dbProvider, + AccountCache accountCache) { + this.self = self; + this.dbProvider = dbProvider; + this.accountCache = accountCache; + } + + @Override + public Response apply(AccountResource rsrc, Input input) throws AuthException, + ResourceNotFoundException, ResourceConflictException, OrmException { + if (self.get() != rsrc.getUser() + && !self.get().getCapabilities().canAdministrateServer()) { + throw new AuthException("not allowed to set HTTP password"); + } + if (input == null) { + input = new Input(); + } + if (rsrc.getUser().getUserName() == null) { + throw new ResourceConflictException("username must be set"); + } + AccountExternalId.Key key = + new AccountExternalId.Key(SCHEME_USERNAME, rsrc.getUser().getUserName()); + AccountExternalId id = dbProvider.get().accountExternalIds().get(key); + if (id == null) { + throw new ResourceNotFoundException(); + } + + String newPassword; + if (input.generate) { + newPassword = generate(); + } else { + if (!Strings.isNullOrEmpty(input.httpPassword) + && !self.get().getCapabilities().canAdministrateServer()) { + throw new AuthException("not allowed to set HTTP password directly, " + + "need to be Gerrit administrator"); + } + newPassword = Strings.emptyToNull(input.httpPassword); + } + + id.setPassword(newPassword); + dbProvider.get().accountExternalIds().update(Collections.singleton(id)); + accountCache.evict(rsrc.getUser().getAccountId()); + + return Strings.isNullOrEmpty(newPassword) + ? Response.none() + : Response.ok(newPassword); + } + + private String generate() { + byte[] rand = new byte[LEN]; + rng.nextBytes(rand); + + byte[] enc = Base64.encodeBase64(rand, false); + StringBuilder r = new StringBuilder(LEN); + for (int i = 0; i < LEN; i++) { + if (enc[i] == '=') { + break; + } + r.append((char) enc[i]); + } + return r.toString(); + } +}