gerrit-server: use hashed passwords for HTTP.
Consequences: * Removes the GET endpoint for the HTTP password * Removes digest authentication * Removes auth.gitBasicAuth config option. With the move to NoteDB, the per-account data (including the HTTP password) will be stored in a branch in the All-Users repo, where it is subject to Gerrit ACLs. Since these are notoriously hard to setup correctly, we want to avoid storing the password in plaintext. With this change, we support hashed passwords, and a schema upgrade populates the existing 'password' field using previous passwords. Tested migration manually: * ran schema upgrade * verified that schema upgrade inserts hashed passwords with gsql. * verified that the password still works with the new code. Tested passwords manually: * verified that correct passwords get accepted when using curl --user. * verified that wrong passwords get rejected when using curl --user. Change-Id: I26f5bcd7848040107e3721eeabf75baeb79c1724
This commit is contained in:
parent
64f54cce18
commit
84d830b5b3
@ -141,6 +141,14 @@ account's full DN, which is discovered by first querying the
|
||||
directory using either an anonymous request, or the configured
|
||||
<<ldap.username,ldap.username>> identity. Gerrit can also use kerberos if
|
||||
<<ldap.authentication,ldap.authentication>> is set to `GSSAPI`.
|
||||
+
|
||||
If link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP`,
|
||||
the randomly generated HTTP password is used for authentication. On the other hand,
|
||||
if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
|
||||
the password in the request is first checked against the HTTP password and, if
|
||||
it does not match, it is then validated against the LDAP password.
|
||||
Service users that only exist in the Gerrit database are authenticated by their
|
||||
HTTP passwords.
|
||||
|
||||
* `LDAP_BIND`
|
||||
+
|
||||
@ -164,6 +172,12 @@ types of data, and can be revoked by users at any time.
|
||||
Site owners have to register their application before getting started. Note
|
||||
that provider specific plugins must be used with this authentication scheme.
|
||||
+
|
||||
Git clients may send OAuth 2 access tokens instead of passwords in the Basic
|
||||
authentication header. Note that provider specific plugins must be installed to
|
||||
facilitate this authentication scheme. If multiple OAuth 2 provider plugins are
|
||||
installed one of them must be selected as default with the
|
||||
`auth.gitOAuthProvider` option.
|
||||
+
|
||||
* `DEVELOPMENT_BECOME_ANY_ACCOUNT`
|
||||
+
|
||||
*DO NOT USE*. Only for use in a development environment.
|
||||
@ -279,7 +293,7 @@ The "Sign In" link will send users directly to this URL.
|
||||
[[auth.httpHeader]]auth.httpHeader::
|
||||
+
|
||||
HTTP header to trust the username from, or unset to select HTTP basic
|
||||
or digest authentication. Only used if `auth.type` is set to `HTTP`.
|
||||
authentication. Only used if `auth.type` is set to `HTTP`.
|
||||
|
||||
[[auth.httpDisplaynameHeader]]auth.httpDisplaynameHeader::
|
||||
+
|
||||
@ -445,45 +459,16 @@ Gerrit to authenticate users. In this case Gerrit will blindly trust
|
||||
the container.
|
||||
+
|
||||
This parameter only affects git over http traffic. If set to false
|
||||
then Gerrit will do the authentication (using DIGEST authentication).
|
||||
then Gerrit will do the authentication (using Basic authentication).
|
||||
+
|
||||
By default this is set to false.
|
||||
|
||||
[[auth.gitBasicAuth]]auth.gitBasicAuth::
|
||||
+
|
||||
If true then Git over HTTP and HTTP/S traffic is authenticated using
|
||||
standard BasicAuth. Depending on the configured `auth.type`, credentials
|
||||
are validated against the randomly generated HTTP password, against LDAP
|
||||
(`auth.type = LDAP`) or against an OAuth 2 provider (`auth.type = OAUTH`).
|
||||
+
|
||||
This parameter affects git over HTTP traffic and access to the REST
|
||||
API. If set to false then Gerrit will authenticate through DIGEST
|
||||
authentication and the randomly generated HTTP password in the Gerrit
|
||||
database.
|
||||
+
|
||||
When `auth.type` is `LDAP`, users should authenticate using their LDAP passwords.
|
||||
However, if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP`,
|
||||
the randomly generated HTTP password is used exclusively. In the other hand,
|
||||
if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
|
||||
the password in the request is first checked against the HTTP password and, if
|
||||
it does not match, it is then validated against the LDAP password.
|
||||
Service users that only exist in the Gerrit database are authenticated by their
|
||||
HTTP passwords.
|
||||
+
|
||||
When `auth.type` is `OAUTH`, Git clients may send OAuth 2 access tokens
|
||||
instead of passwords in the Basic authentication header. Note that provider
|
||||
specific plugins must be installed to facilitate this authentication scheme.
|
||||
If multiple OAuth 2 provider plugins are installed one of them must be
|
||||
selected as default with the `auth.gitOAuthProvider` option.
|
||||
+
|
||||
By default this is set to false.
|
||||
|
||||
[[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
|
||||
+
|
||||
When `auth.type` is `LDAP` and BasicAuth (i.e., link:#auth.gitBasicAuth[`auth.gitBasicAuth`]
|
||||
is set to true), it allows using either the generated HTTP password, the LDAP
|
||||
password or both to authenticate Git over HTTP and REST API requests. The
|
||||
supported values are:
|
||||
When `auth.type` is `LDAP`, it allows using either the generated HTTP password,
|
||||
the LDAP password, or both, to authenticate Git over HTTP and REST API
|
||||
requests. The supported values are:
|
||||
+
|
||||
*`HTTP`
|
||||
+
|
||||
|
@ -88,7 +88,7 @@ To link another identity to an existing account:
|
||||
Login using the other identity can only be performed after the linking is
|
||||
successful.
|
||||
|
||||
== HTTP Basic/Digest Authentication
|
||||
== HTTP Basic Authentication
|
||||
|
||||
When using HTTP authentication, Gerrit assumes that the servlet
|
||||
container or the frontend web server has performed all user
|
||||
|
@ -1443,7 +1443,7 @@ can be accessed from any REST client, i. e.:
|
||||
----
|
||||
curl -X POST -H "Content-Type: application/json" \
|
||||
-d '{message: "François", french: true}' \
|
||||
--digest --user joe:secret \
|
||||
--user joe:secret \
|
||||
http://host:port/a/changes/1/revisions/1/cookbook~say-hello
|
||||
"Bonjour François from change 1, patch set 1!"
|
||||
----
|
||||
@ -2451,18 +2451,18 @@ is an error. Errors are always handled by the Gerrit core UI which
|
||||
shows the error dialog. This means currently plugins cannot do any
|
||||
error handling and e.g. ignore expected errors.
|
||||
|
||||
In the following example the REST endpoint would return '404 Not Found'
|
||||
if there is no HTTP password and the Gerrit core UI would display an
|
||||
error dialog for this. However having no HTTP password is not an error
|
||||
and the plugin may like to handle this case.
|
||||
In the following example the REST endpoint would return '404 Not
|
||||
Found' if the user has no username and the Gerrit core UI would
|
||||
display an error dialog for this. However having no username is
|
||||
not an error and the plugin may like to handle this case.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
new RestApi("accounts").id("self").view("password.http")
|
||||
new RestApi("accounts").id("self").view("username")
|
||||
.get(new AsyncCallback<NativeString>() {
|
||||
|
||||
@Override
|
||||
public void onSuccess(NativeString httpPassword) {
|
||||
public void onSuccess(NativeString username) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ option instead:
|
||||
Example to set a Gerrit project's link:rest-api-projects.html#set-project-description[description]:
|
||||
|
||||
----
|
||||
curl -X PUT --digest --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json; charset=UTF-8" http://localhost:8080/a/projects/myproject/description
|
||||
curl -X PUT --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json; charset=UTF-8" http://localhost:8080/a/projects/myproject/description
|
||||
----
|
||||
|
||||
=== Authentication
|
||||
@ -56,7 +56,7 @@ To test APIs that require authentication, the username and password must be spec
|
||||
the command line:
|
||||
|
||||
----
|
||||
curl --digest --user username:password http://localhost:8080/a/path/to/api/
|
||||
curl --user username:password http://localhost:8080/a/path/to/api/
|
||||
----
|
||||
|
||||
This makes it easy to switch users for testing of permissions.
|
||||
@ -65,7 +65,7 @@ It is also possible to test with a username and password from the `.netrc`
|
||||
file (on Windows, `_netrc`):
|
||||
|
||||
----
|
||||
curl --digest -n http://localhost:8080/a/path/to/api/
|
||||
curl -n http://localhost:8080/a/path/to/api/
|
||||
----
|
||||
|
||||
In both cases, the password should be the user's link:user-upload.html#http[HTTP password].
|
||||
@ -75,7 +75,7 @@ In both cases, the password should be the user's link:user-upload.html#http[HTTP
|
||||
To verify the headers returned from a REST API call, use `curl` in verbose mode:
|
||||
|
||||
----
|
||||
curl -v -n --digest -X DELETE http://localhost:8080/a/path/to/api/
|
||||
curl -v -n -X DELETE http://localhost:8080/a/path/to/api/
|
||||
----
|
||||
|
||||
The headers on both the request and the response will be printed.
|
||||
|
@ -458,31 +458,6 @@ Sets the account state to inactive.
|
||||
|
||||
If the account was already inactive the response is "`409 Conflict`".
|
||||
|
||||
[[get-http-password]]
|
||||
=== Get HTTP Password
|
||||
--
|
||||
'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
|
||||
|
||||
)]}'
|
||||
"Qmxlc21ydCB1YmVyIGFsbGVzIGluIGRlciBXZWx0IQ"
|
||||
----
|
||||
|
||||
If the account does not have an HTTP password the response is "`404 Not Found`".
|
||||
|
||||
[[set-http-password]]
|
||||
=== Set/Generate HTTP Password
|
||||
--
|
||||
@ -1028,12 +1003,12 @@ link:#capability-info[CapabilityInfo] entity.
|
||||
}
|
||||
----
|
||||
|
||||
Administrator that has authenticated with digest authentication:
|
||||
Administrator that has authenticated with basic authentication:
|
||||
|
||||
.Request
|
||||
----
|
||||
GET /a/accounts/self/capabilities HTTP/1.0
|
||||
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
|
||||
Authorization: Basic ABCDECF..
|
||||
----
|
||||
|
||||
.Response
|
||||
@ -1075,7 +1050,7 @@ possible alternative for the caller.
|
||||
.Request
|
||||
----
|
||||
GET /a/accounts/self/capabilities?q=createAccount&q=createGroup HTTP/1.0
|
||||
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
|
||||
Authorization: Basic ABCDEF...
|
||||
----
|
||||
|
||||
.Response
|
||||
|
@ -470,9 +470,9 @@ The cache names are lexicographically sorted.
|
||||
E.g. this could be used to flush all caches:
|
||||
+
|
||||
----
|
||||
for c in $(curl --digest --user jdoe:TNAuLkXsIV7w http://gerrit/a/config/server/caches/?format=TEXT_LIST | base64 -D)
|
||||
for c in $(curl --user jdoe:TNAuLkXsIV7w http://gerrit/a/config/server/caches/?format=TEXT_LIST | base64 -D)
|
||||
do
|
||||
curl --digest --user jdoe:TNAuLkXsIV7w -X POST http://gerrit/a/config/server/caches/$c/flush
|
||||
curl --user jdoe:TNAuLkXsIV7w -X POST http://gerrit/a/config/server/caches/$c/flush
|
||||
done
|
||||
----
|
||||
|
||||
@ -1270,11 +1270,6 @@ type] is `LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
|
||||
The link:config-gerrit.html#auth.httpPasswordUrl[URL to obtain an HTTP
|
||||
password]. Only set if link:config-gerrit.html#auth.type[authentication
|
||||
type] is `CUSTOM_EXTENSION`.
|
||||
|`is_git_basic_auth` |optional, not set if `false`|
|
||||
Whether link:config-gerrit.html#auth.gitBasicAuth[basic authentication
|
||||
is used for Git over HTTP/HTTPS]. Only set if
|
||||
link:config-gerrit.html#auth.type[authentication type] is is `LDAP` or
|
||||
`LDAP_BIND`.
|
||||
|`git_basic_auth_policy` |optional|
|
||||
The link:config-gerrit.html#auth.gitBasicAuthPolicy[policy] to authenticate
|
||||
Git over HTTP and REST API requests when
|
||||
|
@ -87,7 +87,7 @@ To provide the plugin jar as binary data in the request body the
|
||||
following curl command can be used:
|
||||
|
||||
----
|
||||
curl --digest --user admin:TNNuLkWsIV8w -X PUT --data-binary @delete-project-2.8.jar 'http://gerrit:8080/a/plugins/delete-project'
|
||||
curl --user admin:TNNuLkWsIV8w -X PUT --data-binary @delete-project-2.8.jar 'http://gerrit:8080/a/plugins/delete-project'
|
||||
----
|
||||
|
||||
As response a link:#plugin-info[PluginInfo] entity is returned that
|
||||
|
@ -36,10 +36,8 @@ Users (and programs) may authenticate by prefixing the endpoint URL with
|
||||
`/a/`. For example to authenticate to `/projects/`, request the URL
|
||||
`/a/projects/`.
|
||||
|
||||
By default Gerrit uses HTTP digest authentication with the HTTP password
|
||||
from the user's account settings page. HTTP basic authentication is used
|
||||
if link:config-gerrit.html#auth.gitBasicAuth[`auth.gitBasicAuth`] is set
|
||||
to true in the Gerrit configuration.
|
||||
Gerrit uses HTTP basic authentication with the HTTP password from the
|
||||
user's account settings page.
|
||||
|
||||
[[preconditions]]
|
||||
=== Preconditions
|
||||
|
@ -18,10 +18,9 @@ public key, and HTTP/HTTPS.
|
||||
On Gerrit installations that do not support SSH authentication, the
|
||||
user must authenticate via HTTP/HTTPS.
|
||||
|
||||
When link:config-gerrit.html#auth.gitBasicAuth[gitBasicAuth] is enabled,
|
||||
the user is authenticated using standard BasicAuth. Depending on the value of
|
||||
link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy], credentials are
|
||||
validated using:
|
||||
The user is authenticated using standard BasicAuth. Depending on the
|
||||
value of link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy],
|
||||
credentials are validated using:
|
||||
|
||||
* The randomly generated HTTP password on the `HTTP Password` tab
|
||||
in the user settings page if `gitBasicAuthPolicy` is `HTTP`.
|
||||
@ -29,9 +28,10 @@ validated using:
|
||||
* Both, the HTTP and the LDAP passwords (in this order) if `gitBasicAuthPolicy`
|
||||
is `HTTP_LDAP`.
|
||||
|
||||
When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can be
|
||||
accessed within Gerrit by going to `Settings`, and then accessing the `HTTP
|
||||
Password` tab.
|
||||
When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can
|
||||
be regenerated by going to `Settings`, and then accessing the `HTTP
|
||||
Password` tab. Revocation can effectively be done by regenerating the
|
||||
password and then forgetting it.
|
||||
|
||||
For Gerrit installations where an link:config-gerrit.html#auth.httpPasswordUrl[HTTP password URL]
|
||||
is configured, the password can be obtained by clicking on `Obtain Password`
|
||||
|
@ -27,6 +27,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.account.AccountByEmailCache;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.account.GroupCache;
|
||||
import com.google.gerrit.server.account.HashedPassword;
|
||||
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
|
||||
import com.google.gerrit.server.index.account.AccountIndexer;
|
||||
import com.google.gerrit.server.ssh.SshKeyCache;
|
||||
@ -87,7 +88,8 @@ public class AccountCreator {
|
||||
new AccountExternalId(
|
||||
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
|
||||
String httpPass = "http-pass";
|
||||
extUser.setPassword(httpPass);
|
||||
extUser.setPassword(HashedPassword.fromPassword(httpPass).encode());
|
||||
|
||||
db.accountExternalIds().insert(Collections.singleton(extUser));
|
||||
|
||||
if (email != null) {
|
||||
|
@ -88,7 +88,6 @@ public class ServerInfoIT extends AbstractDaemonTest {
|
||||
assertThat(i.auth.registerText).isNull();
|
||||
assertThat(i.auth.editFullNameUrl).isNull();
|
||||
assertThat(i.auth.httpPasswordUrl).isNull();
|
||||
assertThat(i.auth.isGitBasicAuth).isNull();
|
||||
|
||||
// change
|
||||
assertThat(i.change.allowDrafts).isNull();
|
||||
@ -163,7 +162,6 @@ public class ServerInfoIT extends AbstractDaemonTest {
|
||||
assertThat(i.auth.registerText).isNull();
|
||||
assertThat(i.auth.editFullNameUrl).isNull();
|
||||
assertThat(i.auth.httpPasswordUrl).isNull();
|
||||
assertThat(i.auth.isGitBasicAuth).isNull();
|
||||
|
||||
// change
|
||||
assertThat(i.change.allowDrafts).isTrue();
|
||||
|
@ -31,6 +31,5 @@ public class AuthInfo {
|
||||
public String registerText;
|
||||
public String editFullNameUrl;
|
||||
public String httpPasswordUrl;
|
||||
public Boolean isGitBasicAuth;
|
||||
public GitBasicAuthPolicy gitBasicAuthPolicy;
|
||||
}
|
||||
|
@ -42,14 +42,10 @@ public class GitOverHttpModule extends ServletModule {
|
||||
Class<? extends Filter> authFilter;
|
||||
if (authConfig.isTrustContainerAuth()) {
|
||||
authFilter = ContainerAuthFilter.class;
|
||||
} else if (authConfig.isGitBasicAuth()) {
|
||||
if (authConfig.getAuthType() == OAUTH) {
|
||||
authFilter = ProjectOAuthFilter.class;
|
||||
} else {
|
||||
authFilter = ProjectBasicAuthFilter.class;
|
||||
}
|
||||
} else if (authConfig.getAuthType() == OAUTH) {
|
||||
authFilter = ProjectOAuthFilter.class;
|
||||
} else {
|
||||
authFilter = ProjectDigestFilter.class;
|
||||
authFilter = ProjectBasicAuthFilter.class;
|
||||
}
|
||||
|
||||
if (isHttpEnabled()) {
|
||||
|
@ -140,7 +140,7 @@ class ProjectBasicAuthFilter implements Filter {
|
||||
GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
|
||||
if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
|
||||
|| gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
|
||||
if (passwordMatchesTheUserGeneratedOne(who, username, password)) {
|
||||
if (who.checkPassword(password, username)) {
|
||||
return succeedAuthentication(who);
|
||||
}
|
||||
}
|
||||
@ -157,7 +157,7 @@ class ProjectBasicAuthFilter implements Filter {
|
||||
setUserIdentified(whoAuthResult.getAccountId());
|
||||
return true;
|
||||
} catch (NoSuchUserException e) {
|
||||
if (password.equals(who.getPassword(who.getUserName()))) {
|
||||
if (who.checkPassword(password, who.getUserName())) {
|
||||
return succeedAuthentication(who);
|
||||
}
|
||||
log.warn("Authentication failed for " + username, e);
|
||||
@ -193,12 +193,6 @@ class ProjectBasicAuthFilter implements Filter {
|
||||
ws.setAccessPathOk(AccessPath.REST_API, true);
|
||||
}
|
||||
|
||||
private boolean passwordMatchesTheUserGeneratedOne(
|
||||
AccountState who, String username, String password) {
|
||||
String accountPassword = who.getPassword(username);
|
||||
return accountPassword != null && password != null && accountPassword.equals(password);
|
||||
}
|
||||
|
||||
private String encoding(HttpServletRequest req) {
|
||||
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
|
||||
}
|
||||
|
@ -1,337 +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.httpd;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.concurrent.TimeUnit.HOURS;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
||||
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||
import com.google.gerrit.server.AccessPath;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.account.AccountState;
|
||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gwtjsonrpc.server.SignedToken;
|
||||
import com.google.gwtjsonrpc.server.XsrfException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
|
||||
/**
|
||||
* Authenticates the current user by HTTP digest authentication.
|
||||
*
|
||||
* <p>The current HTTP request is authenticated by looking up the username from the Authorization
|
||||
* header and checking the digest response against the stored password. This filter is intended only
|
||||
* to protect the {@link GitOverHttpServlet} and its handled URLs, which provide remote repository
|
||||
* access over HTTP.
|
||||
*
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
|
||||
*/
|
||||
@Singleton
|
||||
class ProjectDigestFilter implements Filter {
|
||||
public static final String REALM_NAME = "Gerrit Code Review";
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
|
||||
private final Provider<String> urlProvider;
|
||||
private final DynamicItem<WebSession> session;
|
||||
private final AccountCache accountCache;
|
||||
private final Config config;
|
||||
private final SignedToken tokens;
|
||||
private ServletContext context;
|
||||
|
||||
@Inject
|
||||
ProjectDigestFilter(
|
||||
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
|
||||
DynamicItem<WebSession> session,
|
||||
AccountCache accountCache,
|
||||
@GerritServerConfig Config config)
|
||||
throws XsrfException {
|
||||
this.urlProvider = urlProvider;
|
||||
this.session = session;
|
||||
this.accountCache = accountCache;
|
||||
this.config = config;
|
||||
this.tokens = new SignedToken((int) SECONDS.convert(1, HOURS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) {
|
||||
context = config.getServletContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
Response rsp = new Response(req, (HttpServletResponse) response);
|
||||
|
||||
if (verify(req, rsp)) {
|
||||
chain.doFilter(req, rsp);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
|
||||
final String hdr = req.getHeader(AUTHORIZATION);
|
||||
if (hdr == null || !hdr.startsWith("Digest ")) {
|
||||
// Allow an anonymous connection through, or it might be using a
|
||||
// session cookie instead of digest authentication.
|
||||
return true;
|
||||
}
|
||||
|
||||
final Map<String, String> p = parseAuthorization(hdr);
|
||||
final String user = p.get("username");
|
||||
final String realm = p.get("realm");
|
||||
final String nonce = p.get("nonce");
|
||||
final String uri = p.get("uri");
|
||||
final String response = p.get("response");
|
||||
final String qop = p.get("qop");
|
||||
final String nc = p.get("nc");
|
||||
final String cnonce = p.get("cnonce");
|
||||
final String method = req.getMethod();
|
||||
|
||||
if (user == null //
|
||||
|| realm == null //
|
||||
|| nonce == null //
|
||||
|| uri == null //
|
||||
|| response == null //
|
||||
|| !"auth".equals(qop) //
|
||||
|| !REALM_NAME.equals(realm)) {
|
||||
context.log("Invalid header: " + AUTHORIZATION + ": " + hdr);
|
||||
rsp.sendError(SC_FORBIDDEN);
|
||||
return false;
|
||||
}
|
||||
|
||||
String username = user;
|
||||
if (config.getBoolean("auth", "userNameToLowerCase", false)) {
|
||||
username = username.toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
final AccountState who = accountCache.getByUsername(username);
|
||||
if (who == null || !who.getAccount().isActive()) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String passwd = who.getPassword(username);
|
||||
if (passwd == null) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String A1 = user + ":" + realm + ":" + passwd;
|
||||
final String A2 = method + ":" + uri;
|
||||
final String expect = KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2));
|
||||
|
||||
if (expect.equals(response)) {
|
||||
try {
|
||||
if (tokens.checkToken(nonce, "") != null) {
|
||||
WebSession ws = session.get();
|
||||
ws.setUserAccountId(who.getAccount().getId());
|
||||
ws.setAccessPathOk(AccessPath.GIT, true);
|
||||
ws.setAccessPathOk(AccessPath.REST_API, true);
|
||||
return true;
|
||||
}
|
||||
rsp.stale = true;
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
} catch (XsrfException e) {
|
||||
context.log("Error validating nonce for digest authentication", e);
|
||||
rsp.sendError(SC_INTERNAL_SERVER_ERROR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String H(String data) {
|
||||
MessageDigest md = newMD5();
|
||||
md.update(data.getBytes(UTF_8));
|
||||
return LHEX(md.digest());
|
||||
}
|
||||
|
||||
private static String KD(String secret, String data) {
|
||||
MessageDigest md = newMD5();
|
||||
md.update(secret.getBytes(UTF_8));
|
||||
md.update((byte) ':');
|
||||
md.update(data.getBytes(UTF_8));
|
||||
return LHEX(md.digest());
|
||||
}
|
||||
|
||||
private static MessageDigest newMD5() {
|
||||
try {
|
||||
return MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("No MD5 available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final char[] LHEX = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
};
|
||||
|
||||
private static String LHEX(byte[] bin) {
|
||||
StringBuilder r = new StringBuilder(bin.length * 2);
|
||||
for (byte b : bin) {
|
||||
r.append(LHEX[(b >>> 4) & 0x0f]);
|
||||
r.append(LHEX[b & 0x0f]);
|
||||
}
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
private Map<String, String> parseAuthorization(String auth) {
|
||||
Map<String, String> p = new HashMap<>();
|
||||
int next = "Digest ".length();
|
||||
while (next < auth.length()) {
|
||||
if (next < auth.length() && auth.charAt(next) == ',') {
|
||||
next++;
|
||||
}
|
||||
while (next < auth.length() && Character.isWhitespace(auth.charAt(next))) {
|
||||
next++;
|
||||
}
|
||||
|
||||
int eq = auth.indexOf('=', next);
|
||||
if (eq < 0 || eq + 1 == auth.length()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final String name = auth.substring(next, eq);
|
||||
final String value;
|
||||
if (auth.charAt(eq + 1) == '"') {
|
||||
int dq = auth.indexOf('"', eq + 2);
|
||||
if (dq < 0) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
value = auth.substring(eq + 2, dq);
|
||||
next = dq + 1;
|
||||
|
||||
} else {
|
||||
int space = auth.indexOf(' ', eq + 1);
|
||||
int comma = auth.indexOf(',', eq + 1);
|
||||
if (space < 0) {
|
||||
space = auth.length();
|
||||
}
|
||||
if (comma < 0) {
|
||||
comma = auth.length();
|
||||
}
|
||||
|
||||
final int e = Math.min(space, comma);
|
||||
value = auth.substring(eq + 1, e);
|
||||
next = e + 1;
|
||||
}
|
||||
p.put(name, value);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private String newNonce() {
|
||||
try {
|
||||
return tokens.newToken("");
|
||||
} catch (XsrfException e) {
|
||||
throw new RuntimeException("Cannot generate new nonce", e);
|
||||
}
|
||||
}
|
||||
|
||||
class Response extends HttpServletResponseWrapper {
|
||||
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
|
||||
private final HttpServletRequest req;
|
||||
Boolean stale;
|
||||
|
||||
Response(HttpServletRequest req, HttpServletResponse rsp) {
|
||||
super(rsp);
|
||||
this.req = req;
|
||||
}
|
||||
|
||||
private void status(int sc) {
|
||||
if (sc == SC_UNAUTHORIZED) {
|
||||
StringBuilder v = new StringBuilder();
|
||||
v.append("Digest");
|
||||
v.append(" realm=\"").append(REALM_NAME).append("\"");
|
||||
|
||||
String url = urlProvider.get();
|
||||
if (url == null) {
|
||||
url = req.getContextPath();
|
||||
if (url != null && !url.isEmpty() && !url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
}
|
||||
if (url != null && !url.isEmpty()) {
|
||||
v.append(", domain=\"").append(url).append("\"");
|
||||
}
|
||||
|
||||
v.append(", qop=\"auth\"");
|
||||
if (stale != null) {
|
||||
v.append(", stale=").append(stale);
|
||||
}
|
||||
v.append(", nonce=\"").append(newNonce()).append("\"");
|
||||
setHeader(WWW_AUTHENTICATE, v.toString());
|
||||
|
||||
} else if (containsHeader(WWW_AUTHENTICATE)) {
|
||||
setHeader(WWW_AUTHENTICATE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc, String msg) throws IOException {
|
||||
status(sc);
|
||||
super.sendError(sc, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendError(int sc) throws IOException {
|
||||
status(sc);
|
||||
super.sendError(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void setStatus(int sc, String sm) {
|
||||
status(sc);
|
||||
super.setStatus(sc, sm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(int sc) {
|
||||
status(sc);
|
||||
super.setStatus(sc);
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ import com.google.gerrit.reviewdb.client.AccountGroupName;
|
||||
import com.google.gerrit.reviewdb.client.AccountSshKey;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.account.AccountState;
|
||||
import com.google.gerrit.server.account.HashedPassword;
|
||||
import com.google.gerrit.server.index.account.AccountIndex;
|
||||
import com.google.gerrit.server.index.account.AccountIndexCollection;
|
||||
import com.google.gwtorm.server.SchemaFactory;
|
||||
@ -95,7 +96,7 @@ public class InitAdminUser implements InitStep {
|
||||
new AccountExternalId(
|
||||
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
|
||||
if (!Strings.isNullOrEmpty(httpPassword)) {
|
||||
extUser.setPassword(httpPassword);
|
||||
extUser.setPassword(HashedPassword.fromPassword(httpPassword).encode());
|
||||
}
|
||||
extIds.add(extUser);
|
||||
db.accountExternalIds().insert(Collections.singleton(extUser));
|
||||
|
@ -87,6 +87,8 @@ public final class AccountExternalId {
|
||||
@Column(id = 3, notNull = false)
|
||||
protected String emailAddress;
|
||||
|
||||
// Encoded version of the hashed and salted password, to be interpreted by the
|
||||
// {@link HashedPassword} class.
|
||||
@Column(id = 4, notNull = false)
|
||||
protected String password;
|
||||
|
||||
@ -140,12 +142,12 @@ public final class AccountExternalId {
|
||||
return null != scheme ? getExternalId().substring(scheme.length() + 1) : null;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
public void setPassword(String hashed) {
|
||||
password = hashed;
|
||||
}
|
||||
|
||||
public void setPassword(String p) {
|
||||
password = p;
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public boolean isTrusted() {
|
||||
|
@ -230,9 +230,11 @@ junit_tests(
|
||||
"//lib:guava",
|
||||
"//lib:guava-retrying",
|
||||
"//lib:protobuf",
|
||||
"//lib/bouncycastle:bcprov",
|
||||
"//lib/dropwizard:dropwizard-core",
|
||||
"//lib/guice:guice-assistedinject",
|
||||
"//lib/prolog:runtime",
|
||||
"//lib/commons:codec",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -18,6 +18,7 @@ import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
|
||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
@ -32,8 +33,13 @@ import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AccountState {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AccountState.class);
|
||||
|
||||
public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
|
||||
a -> a.getAccount().getId();
|
||||
|
||||
@ -70,14 +76,28 @@ public class AccountState {
|
||||
return account.getUserName();
|
||||
}
|
||||
|
||||
/** @return the password matching the requested username; or null. */
|
||||
public String getPassword(String username) {
|
||||
public boolean checkPassword(String password, String username) {
|
||||
if (password == null) {
|
||||
return false;
|
||||
}
|
||||
for (AccountExternalId id : getExternalIds()) {
|
||||
if (id.isScheme(AccountExternalId.SCHEME_USERNAME) && username.equals(id.getSchemeRest())) {
|
||||
return id.getPassword();
|
||||
// Only process the "username:$USER" entry, which is unique.
|
||||
if (!id.isScheme(AccountExternalId.SCHEME_USERNAME) || !username.equals(id.getSchemeRest())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String hashedStr = id.getPassword();
|
||||
if (!Strings.isNullOrEmpty(hashedStr)) {
|
||||
try {
|
||||
return HashedPassword.decode(hashedStr).checkPassword(password);
|
||||
} catch (DecoderException e) {
|
||||
logger.error(
|
||||
String.format("DecoderException for user %s: %s ", username, e.getMessage()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The external identities that identify the account holder. */
|
||||
|
@ -125,7 +125,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
|
||||
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
|
||||
|
||||
if (input.httpPassword != null) {
|
||||
extUser.setPassword(input.httpPassword);
|
||||
extUser.setPassword(HashedPassword.fromPassword(input.httpPassword).encode());
|
||||
}
|
||||
|
||||
if (db.accountExternalIds().get(extUser.getKey()) != null) {
|
||||
|
@ -1,50 +0,0 @@
|
||||
// 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.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class GetHttpPassword implements RestReadView<AccountResource> {
|
||||
|
||||
private final Provider<CurrentUser> self;
|
||||
|
||||
@Inject
|
||||
GetHttpPassword(Provider<CurrentUser> self) {
|
||||
this.self = self;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apply(AccountResource rsrc) throws AuthException, ResourceNotFoundException {
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
// Copyright (C) 2017 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.Preconditions;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.bouncycastle.crypto.generators.BCrypt;
|
||||
import org.bouncycastle.util.Arrays;
|
||||
|
||||
/**
|
||||
* Holds logic for salted, hashed passwords. It uses BCrypt from BouncyCastle, which truncates
|
||||
* passwords at 72 bytes.
|
||||
*/
|
||||
public class HashedPassword {
|
||||
private static final String ALGORITHM_PREFIX = "bcrypt:";
|
||||
private static final SecureRandom secureRandom = new SecureRandom();
|
||||
private static final BaseEncoding codec = BaseEncoding.base64();
|
||||
|
||||
// bcrypt uses 2^cost rounds. Since we use a generated random password, no need
|
||||
// for a high cost.
|
||||
private static final int DEFAULT_COST = 4;
|
||||
|
||||
/**
|
||||
* decodes a hashed password encoded with {@link #encode}.
|
||||
*
|
||||
* @throws DecoderException if input is malformed.
|
||||
*/
|
||||
public static HashedPassword decode(String encoded) throws DecoderException {
|
||||
if (!encoded.startsWith(ALGORITHM_PREFIX)) {
|
||||
throw new DecoderException("unrecognized algorithm");
|
||||
}
|
||||
|
||||
String[] fields = encoded.split(":");
|
||||
if (fields.length != 4) {
|
||||
throw new DecoderException("want 4 fields");
|
||||
}
|
||||
|
||||
Integer cost = Ints.tryParse(fields[1]);
|
||||
if (cost == null) {
|
||||
throw new DecoderException("cost parse failed");
|
||||
}
|
||||
|
||||
if (!(cost >= 4 && cost < 32)) {
|
||||
throw new DecoderException("cost should be 4..31 inclusive, got " + cost);
|
||||
}
|
||||
|
||||
byte[] salt = codec.decode(fields[2]);
|
||||
if (salt.length != 16) {
|
||||
throw new DecoderException("salt should be 16 bytes, got " + salt.length);
|
||||
}
|
||||
return new HashedPassword(codec.decode(fields[3]), salt, cost);
|
||||
}
|
||||
|
||||
private static byte[] hashPassword(String password, byte[] salt, int cost) {
|
||||
byte[] pwBytes = password.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
return BCrypt.generate(pwBytes, salt, cost);
|
||||
}
|
||||
|
||||
public static HashedPassword fromPassword(String password) {
|
||||
byte[] salt = newSalt();
|
||||
|
||||
return new HashedPassword(hashPassword(password, salt, DEFAULT_COST), salt, DEFAULT_COST);
|
||||
}
|
||||
|
||||
private static byte[] newSalt() {
|
||||
byte[] bytes = new byte[16];
|
||||
secureRandom.nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private byte[] salt;
|
||||
private byte[] hashed;
|
||||
private int cost;
|
||||
|
||||
private HashedPassword(byte[] hashed, byte[] salt, int cost) {
|
||||
this.salt = salt;
|
||||
this.hashed = hashed;
|
||||
this.cost = cost;
|
||||
|
||||
Preconditions.checkState(cost >= 4 && cost < 32);
|
||||
|
||||
// salt must be 128 bit.
|
||||
Preconditions.checkState(salt.length == 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the hashed password and its parameters for persistent storage.
|
||||
*
|
||||
* @return one-line string encoding the hash and salt.
|
||||
*/
|
||||
public String encode() {
|
||||
return ALGORITHM_PREFIX + cost + ":" + codec.encode(salt) + ":" + codec.encode(hashed);
|
||||
}
|
||||
|
||||
public boolean checkPassword(String password) {
|
||||
// Constant-time comparison, because we're paranoid.
|
||||
return Arrays.areEqual(hashPassword(password, salt, cost), hashed);
|
||||
}
|
||||
}
|
@ -56,7 +56,6 @@ 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);
|
||||
child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
|
||||
|
@ -113,7 +113,8 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
|
||||
if (id == null) {
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
id.setPassword(newPassword);
|
||||
id.setPassword(HashedPassword.fromPassword(newPassword).encode());
|
||||
|
||||
dbProvider.get().accountExternalIds().update(Collections.singleton(id));
|
||||
accountCache.evict(user.getAccountId());
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
package com.google.gerrit.server.auth;
|
||||
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Defines an abstract request for user authentication to Gerrit. */
|
||||
public abstract class AuthRequest {
|
||||
@ -46,10 +45,4 @@ public abstract class AuthRequest {
|
||||
public final String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void checkPassword(String pwd) throws AuthException {
|
||||
if (!Objects.equals(getPassword(), pwd)) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ public class InternalAuthBackend implements AuthBackend {
|
||||
return "gerrit";
|
||||
}
|
||||
|
||||
// TODO(gerritcodereview-team): This function has no coverage.
|
||||
@Override
|
||||
public AuthUser authenticate(AuthRequest req)
|
||||
throws MissingCredentialsException, InvalidCredentialsException, UnknownUserException,
|
||||
@ -63,7 +64,9 @@ public class InternalAuthBackend implements AuthBackend {
|
||||
+ ": account inactive or not provisioned in Gerrit");
|
||||
}
|
||||
|
||||
req.checkPassword(who.getPassword(username));
|
||||
if (!who.checkPassword(req.getPassword(), username)) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
return new AuthUser(AuthUser.UUID.create(username), username);
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,6 @@ public class AuthConfig {
|
||||
private final boolean trustContainerAuth;
|
||||
private final boolean enableRunAs;
|
||||
private final boolean userNameToLowerCase;
|
||||
private final boolean gitBasicAuth;
|
||||
private final boolean useContributorAgreements;
|
||||
private final String loginUrl;
|
||||
private final String loginText;
|
||||
@ -88,7 +87,6 @@ public class AuthConfig {
|
||||
cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
|
||||
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
|
||||
enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
|
||||
gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
|
||||
gitBasicAuthPolicy = getBasicAuthPolicy(cfg);
|
||||
useContributorAgreements = cfg.getBoolean("auth", "contributoragreements", false);
|
||||
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
|
||||
@ -223,11 +221,6 @@ public class AuthConfig {
|
||||
return userNameToLowerCase;
|
||||
}
|
||||
|
||||
/** Whether git-over-http should use Gerrit basic authentication scheme. */
|
||||
public boolean isGitBasicAuth() {
|
||||
return gitBasicAuth;
|
||||
}
|
||||
|
||||
public GitBasicAuthPolicy getGitBasicAuthPolicy() {
|
||||
return gitBasicAuthPolicy;
|
||||
}
|
||||
|
@ -156,7 +156,6 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
|
||||
info.useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
|
||||
info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
|
||||
info.switchAccountUrl = cfg.getSwitchAccountUrl();
|
||||
info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
|
||||
info.gitBasicAuthPolicy = cfg.getGitBasicAuthPolicy();
|
||||
|
||||
if (info.useContributorAgreements != null) {
|
||||
|
@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit;
|
||||
/** A version of the database schema. */
|
||||
public abstract class SchemaVersion {
|
||||
/** The current schema version. */
|
||||
public static final Class<Schema_141> C = Schema_141.class;
|
||||
public static final Class<Schema_142> C = Schema_142.class;
|
||||
|
||||
public static int getBinaryVersion() {
|
||||
return guessVersion(C);
|
||||
|
@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2017 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.schema;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.account.HashedPassword;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
public class Schema_142 extends SchemaVersion {
|
||||
@Inject
|
||||
Schema_142(Provider<Schema_141> prior) {
|
||||
super(prior);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
|
||||
List<AccountExternalId> newIds = db.accountExternalIds().all().toList();
|
||||
for (AccountExternalId id : newIds) {
|
||||
if (!id.isScheme(AccountExternalId.SCHEME_USERNAME)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String password = id.getPassword();
|
||||
if (password != null) {
|
||||
HashedPassword hashed = HashedPassword.fromPassword(password);
|
||||
id.setPassword(hashed.encode());
|
||||
}
|
||||
}
|
||||
|
||||
db.accountExternalIds().upsert(newIds);
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl --digest -u $gerrituser -w '%{http_code}' -o preview \
|
||||
curl -u $gerrituser -w '%{http_code}' -o preview \
|
||||
$server/a/changes/$changeId/revisions/current/preview_submit?format=tgz >http_code
|
||||
if ! grep 200 http_code >/dev/null
|
||||
then
|
||||
|
@ -0,0 +1,64 @@
|
||||
// Copyright (C) 2017 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.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.junit.Test;
|
||||
|
||||
public class HashedPasswordTest {
|
||||
|
||||
@Test
|
||||
public void encodeOneLine() throws Exception {
|
||||
String password = "secret";
|
||||
HashedPassword hashed = HashedPassword.fromPassword(password);
|
||||
assertThat(hashed.encode()).doesNotContain("\n");
|
||||
assertThat(hashed.encode()).doesNotContain("\r");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodeDecode() throws Exception {
|
||||
String password = "secret";
|
||||
HashedPassword hashed = HashedPassword.fromPassword(password);
|
||||
HashedPassword roundtrip = HashedPassword.decode(hashed.encode());
|
||||
assertThat(hashed.encode()).isEqualTo(roundtrip.encode());
|
||||
assertThat(roundtrip.checkPassword(password)).isTrue();
|
||||
assertThat(roundtrip.checkPassword("not the password")).isFalse();
|
||||
}
|
||||
|
||||
@Test(expected = DecoderException.class)
|
||||
public void invalidDecode() throws Exception {
|
||||
HashedPassword.decode("invalid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lengthLimit() throws Exception {
|
||||
String password = Strings.repeat("1", 72);
|
||||
|
||||
// make sure it fits in varchar(255).
|
||||
assertThat(HashedPassword.fromPassword(password).encode().length()).isLessThan(255);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicFunctionality() throws Exception {
|
||||
String password = "secret";
|
||||
HashedPassword hashed = HashedPassword.fromPassword(password);
|
||||
|
||||
assertThat(hashed.checkPassword("false")).isFalse();
|
||||
assertThat(hashed.checkPassword(password)).isTrue();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user