Merge changes from topic 'http-password'
* changes: Migrate external IDs to NoteDb (part 1) gerrit-server: use hashed passwords for HTTP. AccountByEmailCacheImpl: Consider emails from all external IDs on load * submodules: * Update plugins/cookbook-plugin from branch 'master' - Merge "Remove for HTTP digest auth from examples." - Remove for HTTP digest auth from examples. Change-Id: I495ee8140cbe2ae12510a4d4cbc2c8360b135b33
This commit is contained in:
@@ -141,6 +141,14 @@ account's full DN, which is discovered by first querying the
|
|||||||
directory using either an anonymous request, or the configured
|
directory using either an anonymous request, or the configured
|
||||||
<<ldap.username,ldap.username>> identity. Gerrit can also use kerberos if
|
<<ldap.username,ldap.username>> identity. Gerrit can also use kerberos if
|
||||||
<<ldap.authentication,ldap.authentication>> is set to `GSSAPI`.
|
<<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`
|
* `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
|
Site owners have to register their application before getting started. Note
|
||||||
that provider specific plugins must be used with this authentication scheme.
|
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`
|
* `DEVELOPMENT_BECOME_ANY_ACCOUNT`
|
||||||
+
|
+
|
||||||
*DO NOT USE*. Only for use in a development environment.
|
*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::
|
[[auth.httpHeader]]auth.httpHeader::
|
||||||
+
|
+
|
||||||
HTTP header to trust the username from, or unset to select HTTP basic
|
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::
|
[[auth.httpDisplaynameHeader]]auth.httpDisplaynameHeader::
|
||||||
+
|
+
|
||||||
@@ -445,45 +459,16 @@ Gerrit to authenticate users. In this case Gerrit will blindly trust
|
|||||||
the container.
|
the container.
|
||||||
+
|
+
|
||||||
This parameter only affects git over http traffic. If set to false
|
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.
|
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::
|
[[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
|
||||||
+
|
+
|
||||||
When `auth.type` is `LDAP` and BasicAuth (i.e., link:#auth.gitBasicAuth[`auth.gitBasicAuth`]
|
When `auth.type` is `LDAP`, it allows using either the generated HTTP password,
|
||||||
is set to true), it allows using either the generated HTTP password, the LDAP
|
the LDAP password, or both, to authenticate Git over HTTP and REST API
|
||||||
password or both to authenticate Git over HTTP and REST API requests. The
|
requests. The supported values are:
|
||||||
supported values are:
|
|
||||||
+
|
+
|
||||||
*`HTTP`
|
*`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
|
Login using the other identity can only be performed after the linking is
|
||||||
successful.
|
successful.
|
||||||
|
|
||||||
== HTTP Basic/Digest Authentication
|
== HTTP Basic Authentication
|
||||||
|
|
||||||
When using HTTP authentication, Gerrit assumes that the servlet
|
When using HTTP authentication, Gerrit assumes that the servlet
|
||||||
container or the frontend web server has performed all user
|
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" \
|
curl -X POST -H "Content-Type: application/json" \
|
||||||
-d '{message: "François", french: true}' \
|
-d '{message: "François", french: true}' \
|
||||||
--digest --user joe:secret \
|
--user joe:secret \
|
||||||
http://host:port/a/changes/1/revisions/1/cookbook~say-hello
|
http://host:port/a/changes/1/revisions/1/cookbook~say-hello
|
||||||
"Bonjour François from change 1, patch set 1!"
|
"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
|
shows the error dialog. This means currently plugins cannot do any
|
||||||
error handling and e.g. ignore expected errors.
|
error handling and e.g. ignore expected errors.
|
||||||
|
|
||||||
In the following example the REST endpoint would return '404 Not Found'
|
In the following example the REST endpoint would return '404 Not
|
||||||
if there is no HTTP password and the Gerrit core UI would display an
|
Found' if the user has no username and the Gerrit core UI would
|
||||||
error dialog for this. However having no HTTP password is not an error
|
display an error dialog for this. However having no username is
|
||||||
and the plugin may like to handle this case.
|
not an error and the plugin may like to handle this case.
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
new RestApi("accounts").id("self").view("password.http")
|
new RestApi("accounts").id("self").view("username")
|
||||||
.get(new AsyncCallback<NativeString>() {
|
.get(new AsyncCallback<NativeString>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(NativeString httpPassword) {
|
public void onSuccess(NativeString username) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -47,7 +47,7 @@ option instead:
|
|||||||
Example to set a Gerrit project's link:rest-api-projects.html#set-project-description[description]:
|
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
|
=== Authentication
|
||||||
@@ -56,7 +56,7 @@ To test APIs that require authentication, the username and password must be spec
|
|||||||
the command line:
|
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.
|
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`):
|
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].
|
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:
|
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.
|
The headers on both the request and the response will be printed.
|
||||||
|
@@ -9,7 +9,6 @@ account to lower case
|
|||||||
--
|
--
|
||||||
_java_ -jar gerrit.war _LocalUsernamesToLowerCase
|
_java_ -jar gerrit.war _LocalUsernamesToLowerCase
|
||||||
-d <SITE_PATH>
|
-d <SITE_PATH>
|
||||||
[--threads]
|
|
||||||
--
|
--
|
||||||
|
|
||||||
== DESCRIPTION
|
== DESCRIPTION
|
||||||
@@ -40,10 +39,6 @@ must be run by itself.
|
|||||||
Location of the gerrit.config file, and all other per-site
|
Location of the gerrit.config file, and all other per-site
|
||||||
configuration data, supporting libraries and log files.
|
configuration data, supporting libraries and log files.
|
||||||
|
|
||||||
--threads::
|
|
||||||
Number of threads to perform the scan work with. Defaults to
|
|
||||||
twice the number of CPUs available.
|
|
||||||
|
|
||||||
== CONTEXT
|
== CONTEXT
|
||||||
This command can only be run on a server which has direct
|
This command can only be run on a server which has direct
|
||||||
connectivity to the metadata database.
|
connectivity to the metadata database.
|
||||||
|
@@ -458,31 +458,6 @@ Sets the account state to inactive.
|
|||||||
|
|
||||||
If the account was already inactive the response is "`409 Conflict`".
|
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-http-password]]
|
||||||
=== Set/Generate 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
|
.Request
|
||||||
----
|
----
|
||||||
GET /a/accounts/self/capabilities HTTP/1.0
|
GET /a/accounts/self/capabilities HTTP/1.0
|
||||||
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
|
Authorization: Basic ABCDECF..
|
||||||
----
|
----
|
||||||
|
|
||||||
.Response
|
.Response
|
||||||
@@ -1075,7 +1050,7 @@ possible alternative for the caller.
|
|||||||
.Request
|
.Request
|
||||||
----
|
----
|
||||||
GET /a/accounts/self/capabilities?q=createAccount&q=createGroup HTTP/1.0
|
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
|
.Response
|
||||||
|
@@ -470,9 +470,9 @@ The cache names are lexicographically sorted.
|
|||||||
E.g. this could be used to flush all caches:
|
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
|
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
|
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
|
The link:config-gerrit.html#auth.httpPasswordUrl[URL to obtain an HTTP
|
||||||
password]. Only set if link:config-gerrit.html#auth.type[authentication
|
password]. Only set if link:config-gerrit.html#auth.type[authentication
|
||||||
type] is `CUSTOM_EXTENSION`.
|
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|
|
|`git_basic_auth_policy` |optional|
|
||||||
The link:config-gerrit.html#auth.gitBasicAuthPolicy[policy] to authenticate
|
The link:config-gerrit.html#auth.gitBasicAuthPolicy[policy] to authenticate
|
||||||
Git over HTTP and REST API requests when
|
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:
|
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
|
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/`. For example to authenticate to `/projects/`, request the URL
|
||||||
`/a/projects/`.
|
`/a/projects/`.
|
||||||
|
|
||||||
By default Gerrit uses HTTP digest authentication with the HTTP password
|
Gerrit uses HTTP basic authentication with the HTTP password from the
|
||||||
from the user's account settings page. HTTP basic authentication is used
|
user's account settings page.
|
||||||
if link:config-gerrit.html#auth.gitBasicAuth[`auth.gitBasicAuth`] is set
|
|
||||||
to true in the Gerrit configuration.
|
|
||||||
|
|
||||||
[[preconditions]]
|
[[preconditions]]
|
||||||
=== Preconditions
|
=== Preconditions
|
||||||
|
@@ -18,10 +18,9 @@ public key, and HTTP/HTTPS.
|
|||||||
On Gerrit installations that do not support SSH authentication, the
|
On Gerrit installations that do not support SSH authentication, the
|
||||||
user must authenticate via HTTP/HTTPS.
|
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
|
||||||
the user is authenticated using standard BasicAuth. Depending on the value of
|
value of link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy],
|
||||||
link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy], credentials are
|
credentials are validated using:
|
||||||
validated using:
|
|
||||||
|
|
||||||
* The randomly generated HTTP password on the `HTTP Password` tab
|
* The randomly generated HTTP password on the `HTTP Password` tab
|
||||||
in the user settings page if `gitBasicAuthPolicy` is `HTTP`.
|
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`
|
* Both, the HTTP and the LDAP passwords (in this order) if `gitBasicAuthPolicy`
|
||||||
is `HTTP_LDAP`.
|
is `HTTP_LDAP`.
|
||||||
|
|
||||||
When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can be
|
When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can
|
||||||
accessed within Gerrit by going to `Settings`, and then accessing the `HTTP
|
be regenerated by going to `Settings`, and then accessing the `HTTP
|
||||||
Password` tab.
|
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]
|
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`
|
is configured, the password can be obtained by clicking on `Obtain Password`
|
||||||
|
@@ -20,12 +20,13 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
|
|||||||
|
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.account.AccountByEmailCache;
|
import com.google.gerrit.server.account.AccountByEmailCache;
|
||||||
import com.google.gerrit.server.account.AccountCache;
|
import com.google.gerrit.server.account.AccountCache;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsUpdate;
|
||||||
import com.google.gerrit.server.account.GroupCache;
|
import com.google.gerrit.server.account.GroupCache;
|
||||||
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
|
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
|
||||||
import com.google.gerrit.server.index.account.AccountIndexer;
|
import com.google.gerrit.server.index.account.AccountIndexer;
|
||||||
@@ -39,8 +40,10 @@ import com.jcraft.jsch.JSchException;
|
|||||||
import com.jcraft.jsch.KeyPair;
|
import com.jcraft.jsch.KeyPair;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -54,6 +57,7 @@ public class AccountCreator {
|
|||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
private final AccountByEmailCache byEmailCache;
|
private final AccountByEmailCache byEmailCache;
|
||||||
private final AccountIndexer indexer;
|
private final AccountIndexer indexer;
|
||||||
|
private final ExternalIdsUpdate.Server externalIdsUpdate;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AccountCreator(
|
AccountCreator(
|
||||||
@@ -63,7 +67,8 @@ public class AccountCreator {
|
|||||||
SshKeyCache sshKeyCache,
|
SshKeyCache sshKeyCache,
|
||||||
AccountCache accountCache,
|
AccountCache accountCache,
|
||||||
AccountByEmailCache byEmailCache,
|
AccountByEmailCache byEmailCache,
|
||||||
AccountIndexer indexer) {
|
AccountIndexer indexer,
|
||||||
|
ExternalIdsUpdate.Server externalIdsUpdate) {
|
||||||
accounts = new HashMap<>();
|
accounts = new HashMap<>();
|
||||||
reviewDbProvider = schema;
|
reviewDbProvider = schema;
|
||||||
this.authorizedKeys = authorizedKeys;
|
this.authorizedKeys = authorizedKeys;
|
||||||
@@ -72,6 +77,7 @@ public class AccountCreator {
|
|||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
this.byEmailCache = byEmailCache;
|
this.byEmailCache = byEmailCache;
|
||||||
this.indexer = indexer;
|
this.indexer = indexer;
|
||||||
|
this.externalIdsUpdate = externalIdsUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized TestAccount create(
|
public synchronized TestAccount create(
|
||||||
@@ -83,18 +89,14 @@ public class AccountCreator {
|
|||||||
try (ReviewDb db = reviewDbProvider.open()) {
|
try (ReviewDb db = reviewDbProvider.open()) {
|
||||||
Account.Id id = new Account.Id(db.nextAccountId());
|
Account.Id id = new Account.Id(db.nextAccountId());
|
||||||
|
|
||||||
AccountExternalId extUser =
|
List<ExternalId> extIds = new ArrayList<>(2);
|
||||||
new AccountExternalId(
|
|
||||||
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
|
|
||||||
String httpPass = "http-pass";
|
String httpPass = "http-pass";
|
||||||
extUser.setPassword(httpPass);
|
extIds.add(ExternalId.createUsername(username, id, httpPass));
|
||||||
db.accountExternalIds().insert(Collections.singleton(extUser));
|
|
||||||
|
|
||||||
if (email != null) {
|
if (email != null) {
|
||||||
AccountExternalId extMailto = new AccountExternalId(id, getEmailKey(email));
|
extIds.add(ExternalId.createEmail(id, email));
|
||||||
extMailto.setEmailAddress(email);
|
|
||||||
db.accountExternalIds().insert(Collections.singleton(extMailto));
|
|
||||||
}
|
}
|
||||||
|
externalIdsUpdate.create().insert(db, extIds);
|
||||||
|
|
||||||
Account a = new Account(id, TimeUtil.nowTs());
|
Account a = new Account(id, TimeUtil.nowTs());
|
||||||
a.setFullName(fullName);
|
a.setFullName(fullName);
|
||||||
@@ -157,10 +159,6 @@ public class AccountCreator {
|
|||||||
return checkNotNull(accounts.get(username), "No TestAccount created for %s", username);
|
return checkNotNull(accounts.get(username), "No TestAccount created for %s", username);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccountExternalId.Key getEmailKey(String email) {
|
|
||||||
return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static KeyPair genSshKey() throws JSchException {
|
public static KeyPair genSshKey() throws JSchException {
|
||||||
JSch jsch = new JSch();
|
JSch jsch = new JSch();
|
||||||
return KeyPair.genKeyPair(jsch, KeyPair.RSA);
|
return KeyPair.genKeyPair(jsch, KeyPair.RSA);
|
||||||
|
@@ -35,6 +35,7 @@ import static org.junit.Assert.fail;
|
|||||||
import com.google.common.collect.FluentIterable;
|
import com.google.common.collect.FluentIterable;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||||
import com.google.gerrit.acceptance.AccountCreator;
|
import com.google.gerrit.acceptance.AccountCreator;
|
||||||
@@ -61,8 +62,10 @@ import com.google.gerrit.gpg.PublicKeyStore;
|
|||||||
import com.google.gerrit.gpg.server.GpgKeys;
|
import com.google.gerrit.gpg.server.GpgKeys;
|
||||||
import com.google.gerrit.gpg.testutil.TestKey;
|
import com.google.gerrit.gpg.testutil.TestKey;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.RefNames;
|
import com.google.gerrit.reviewdb.client.RefNames;
|
||||||
|
import com.google.gerrit.server.account.AccountByEmailCache;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsUpdate;
|
||||||
import com.google.gerrit.server.account.WatchConfig;
|
import com.google.gerrit.server.account.WatchConfig;
|
||||||
import com.google.gerrit.server.account.WatchConfig.NotifyType;
|
import com.google.gerrit.server.account.WatchConfig.NotifyType;
|
||||||
import com.google.gerrit.server.config.AllUsersName;
|
import com.google.gerrit.server.config.AllUsersName;
|
||||||
@@ -77,10 +80,10 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||||
@@ -111,10 +114,17 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
|
|
||||||
@Inject private AllUsersName allUsers;
|
@Inject private AllUsersName allUsers;
|
||||||
|
|
||||||
private List<AccountExternalId> savedExternalIds;
|
@Inject private AccountByEmailCache byEmailCache;
|
||||||
|
|
||||||
|
@Inject private ExternalIdsUpdate.User externalIdsUpdateFactory;
|
||||||
|
|
||||||
|
private ExternalIdsUpdate externalIdsUpdate;
|
||||||
|
private List<ExternalId> savedExternalIds;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void saveExternalIds() throws Exception {
|
public void saveExternalIds() throws Exception {
|
||||||
|
externalIdsUpdate = externalIdsUpdateFactory.create();
|
||||||
|
|
||||||
savedExternalIds = new ArrayList<>();
|
savedExternalIds = new ArrayList<>();
|
||||||
savedExternalIds.addAll(getExternalIds(admin));
|
savedExternalIds.addAll(getExternalIds(admin));
|
||||||
savedExternalIds.addAll(getExternalIds(user));
|
savedExternalIds.addAll(getExternalIds(user));
|
||||||
@@ -126,9 +136,9 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
// savedExternalIds is null when we don't run SSH tests and the assume in
|
// savedExternalIds is null when we don't run SSH tests and the assume in
|
||||||
// @Before in AbstractDaemonTest prevents this class' @Before method from
|
// @Before in AbstractDaemonTest prevents this class' @Before method from
|
||||||
// being executed.
|
// being executed.
|
||||||
db.accountExternalIds().delete(getExternalIds(admin));
|
externalIdsUpdate.delete(db, getExternalIds(admin));
|
||||||
db.accountExternalIds().delete(getExternalIds(user));
|
externalIdsUpdate.delete(db, getExternalIds(user));
|
||||||
db.accountExternalIds().insert(savedExternalIds);
|
externalIdsUpdate.insert(db, savedExternalIds);
|
||||||
}
|
}
|
||||||
accountCache.evict(admin.getId());
|
accountCache.evict(admin.getId());
|
||||||
accountCache.evict(user.getId());
|
accountCache.evict(user.getId());
|
||||||
@@ -146,7 +156,7 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Collection<AccountExternalId> getExternalIds(TestAccount account) throws Exception {
|
private Collection<ExternalId> getExternalIds(TestAccount account) throws Exception {
|
||||||
return accountCache.get(account.getId()).getExternalIds();
|
return accountCache.get(account.getId()).getExternalIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,11 +450,11 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
String email = "foo.bar@example.com";
|
String email = "foo.bar@example.com";
|
||||||
String extId1 = "foo:bar";
|
String extId1 = "foo:bar";
|
||||||
String extId2 = "foo:baz";
|
String extId2 = "foo:baz";
|
||||||
db.accountExternalIds()
|
List<ExternalId> extIds =
|
||||||
.insert(
|
ImmutableList.of(
|
||||||
ImmutableList.of(
|
ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email),
|
||||||
createExternalIdWithEmail(extId1, email),
|
ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email));
|
||||||
createExternalIdWithEmail(extId2, email)));
|
externalIdsUpdateFactory.create().insert(db, extIds);
|
||||||
accountCache.evict(admin.id);
|
accountCache.evict(admin.id);
|
||||||
assertThat(
|
assertThat(
|
||||||
gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
|
gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
|
||||||
@@ -486,6 +496,29 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
gApi.accounts().id(admin.id.get()).deleteEmail(admin.email);
|
gApi.accounts().id(admin.id.get()).deleteEmail(admin.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lookUpFromCacheByEmail() throws Exception {
|
||||||
|
// exact match with scheme "mailto:"
|
||||||
|
assertEmail(byEmailCache.get(admin.email), admin);
|
||||||
|
|
||||||
|
// exact match with other scheme
|
||||||
|
String email = "foo.bar@example.com";
|
||||||
|
externalIdsUpdateFactory
|
||||||
|
.create()
|
||||||
|
.insert(db, ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email));
|
||||||
|
accountCache.evict(admin.id);
|
||||||
|
assertEmail(byEmailCache.get(email), admin);
|
||||||
|
|
||||||
|
// wrong case doesn't match
|
||||||
|
assertThat(byEmailCache.get(admin.email.toUpperCase(Locale.US))).isEmpty();
|
||||||
|
|
||||||
|
// prefix doesn't match
|
||||||
|
assertThat(byEmailCache.get(admin.email.substring(0, admin.email.indexOf('@')))).isEmpty();
|
||||||
|
|
||||||
|
// non-existing doesn't match
|
||||||
|
assertThat(byEmailCache.get("non-existing@example.com")).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void putStatus() throws Exception {
|
public void putStatus() throws Exception {
|
||||||
List<String> statuses = ImmutableList.of("OOO", "Busy");
|
List<String> statuses = ImmutableList.of("OOO", "Busy");
|
||||||
@@ -680,10 +713,7 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
public void addOtherUsersGpgKey_Conflict() throws Exception {
|
public void addOtherUsersGpgKey_Conflict() throws Exception {
|
||||||
// Both users have a matching external ID for this key.
|
// Both users have a matching external ID for this key.
|
||||||
addExternalIdEmail(admin, "test5@example.com");
|
addExternalIdEmail(admin, "test5@example.com");
|
||||||
AccountExternalId extId =
|
externalIdsUpdate.insert(db, ExternalId.create("foo", "myId", user.getId()));
|
||||||
new AccountExternalId(user.getId(), new AccountExternalId.Key("foo:myId"));
|
|
||||||
|
|
||||||
db.accountExternalIds().insert(Collections.singleton(extId));
|
|
||||||
accountCache.evict(user.getId());
|
accountCache.evict(user.getId());
|
||||||
|
|
||||||
TestKey key = validKeyWithSecondUserId();
|
TestKey key = validKeyWithSecondUserId();
|
||||||
@@ -883,7 +913,7 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
Iterable<String> expectedFps =
|
Iterable<String> expectedFps =
|
||||||
expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
|
expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
|
||||||
Iterable<String> actualFps =
|
Iterable<String> actualFps =
|
||||||
GpgKeys.getGpgExtIds(db, currAccountId).transform(AccountExternalId::getSchemeRest);
|
GpgKeys.getGpgExtIds(db, currAccountId).transform(e -> e.key().id());
|
||||||
assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps);
|
assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps);
|
||||||
|
|
||||||
// Check raw stored keys.
|
// Check raw stored keys.
|
||||||
@@ -908,11 +938,9 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
|
|
||||||
private void addExternalIdEmail(TestAccount account, String email) throws Exception {
|
private void addExternalIdEmail(TestAccount account, String email) throws Exception {
|
||||||
checkNotNull(email);
|
checkNotNull(email);
|
||||||
AccountExternalId extId =
|
externalIdsUpdate.insert(
|
||||||
new AccountExternalId(account.getId(), new AccountExternalId.Key(name("test"), email));
|
db, ExternalId.createWithEmail(name("test"), email, account.getId(), email));
|
||||||
extId.setEmailAddress(email);
|
// Clear saved AccountState and ExternalIds.
|
||||||
db.accountExternalIds().insert(Collections.singleton(extId));
|
|
||||||
// Clear saved AccountState and AccountExternalIds.
|
|
||||||
accountCache.evict(account.getId());
|
accountCache.evict(account.getId());
|
||||||
setApiUser(account);
|
setApiUser(account);
|
||||||
}
|
}
|
||||||
@@ -932,9 +960,8 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
return gApi.accounts().self().getEmails().stream().map(e -> e.email).collect(toSet());
|
return gApi.accounts().self().getEmails().stream().map(e -> e.email).collect(toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccountExternalId createExternalIdWithEmail(String id, String email) {
|
private void assertEmail(Set<Account.Id> accounts, TestAccount expectedAccount) {
|
||||||
AccountExternalId extId = new AccountExternalId(admin.id, new AccountExternalId.Key(id));
|
assertThat(accounts).hasSize(1);
|
||||||
extId.setEmailAddress(email);
|
assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
|
||||||
return extId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,30 +15,64 @@
|
|||||||
package com.google.gerrit.acceptance.rest.account;
|
package com.google.gerrit.acceptance.rest.account;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static com.google.gerrit.acceptance.GitUtil.fetch;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import com.github.rholder.retry.BlockStrategy;
|
||||||
|
import com.github.rholder.retry.Retryer;
|
||||||
|
import com.github.rholder.retry.RetryerBuilder;
|
||||||
|
import com.github.rholder.retry.StopStrategies;
|
||||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||||
|
import com.google.gerrit.acceptance.PushOneCommit;
|
||||||
import com.google.gerrit.acceptance.RestResponse;
|
import com.google.gerrit.acceptance.RestResponse;
|
||||||
import com.google.gerrit.acceptance.Sandboxed;
|
import com.google.gerrit.acceptance.Sandboxed;
|
||||||
|
import com.google.gerrit.common.data.GlobalCapability;
|
||||||
|
import com.google.gerrit.common.data.Permission;
|
||||||
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
|
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
import com.google.gerrit.reviewdb.client.RefNames;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIds;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsUpdate;
|
||||||
|
import com.google.gerrit.server.config.AllUsersName;
|
||||||
|
import com.google.gerrit.server.git.LockFailureException;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.eclipse.jgit.api.errors.TransportException;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
|
||||||
|
import org.eclipse.jgit.junit.TestRepository;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@Sandboxed
|
@Sandboxed
|
||||||
public class ExternalIdIT extends AbstractDaemonTest {
|
public class ExternalIdIT extends AbstractDaemonTest {
|
||||||
|
@Inject private AllUsersName allUsers;
|
||||||
|
|
||||||
|
@Inject private ExternalIdsUpdate.Server extIdsUpdate;
|
||||||
|
|
||||||
|
@Inject private ExternalIds externalIds;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getExternalIDs() throws Exception {
|
public void getExternalIDs() throws Exception {
|
||||||
Collection<AccountExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds();
|
Collection<ExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds();
|
||||||
|
|
||||||
List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>();
|
List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>();
|
||||||
for (AccountExternalId id : expectedIds) {
|
for (ExternalId id : expectedIds) {
|
||||||
id.setCanDelete(!id.getExternalId().equals("username:" + user.username));
|
AccountExternalIdInfo info = new AccountExternalIdInfo();
|
||||||
id.setTrusted(true);
|
info.identity = id.key().get();
|
||||||
expectedIdInfos.add(toInfo(id));
|
info.emailAddress = id.email();
|
||||||
|
info.canDelete = !id.isScheme(SCHEME_USERNAME) ? true : null;
|
||||||
|
info.trusted = true;
|
||||||
|
expectedIdInfos.add(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
RestResponse response = userRestSession.get("/accounts/self/external.ids");
|
RestResponse response = userRestSession.get("/accounts/self/external.ids");
|
||||||
@@ -102,12 +136,119 @@ public class ExternalIdIT extends AbstractDaemonTest {
|
|||||||
.isEqualTo(String.format("External id %s does not exist", externalIdStr));
|
.isEqualTo(String.format("External id %s does not exist", externalIdStr));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AccountExternalIdInfo toInfo(AccountExternalId id) {
|
@Test
|
||||||
AccountExternalIdInfo info = new AccountExternalIdInfo();
|
public void fetchExternalIdsBranch() throws Exception {
|
||||||
info.identity = id.getExternalId();
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
|
||||||
info.emailAddress = id.getEmailAddress();
|
|
||||||
info.trusted = id.isTrusted() ? true : null;
|
// refs/meta/external-ids is only visible to users with the 'Access Database' capability
|
||||||
info.canDelete = id.canDelete() ? true : null;
|
try {
|
||||||
return info;
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
fail("expected TransportException");
|
||||||
|
} catch (TransportException e) {
|
||||||
|
assertThat(e.getMessage())
|
||||||
|
.isEqualTo(
|
||||||
|
"Remote does not have " + RefNames.REFS_EXTERNAL_IDS + " available for fetch.");
|
||||||
|
}
|
||||||
|
|
||||||
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||||||
|
|
||||||
|
// re-clone to get new request context, otherwise the old global capabilities are still cached
|
||||||
|
// in the IdentifiedUser object
|
||||||
|
allUsersRepo = cloneProject(allUsers, user);
|
||||||
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pushToExternalIdsBranch() throws Exception {
|
||||||
|
grant(Permission.READ, allUsers, RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
grant(Permission.PUSH, allUsers, RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
|
||||||
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||||||
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":externalIds");
|
||||||
|
allUsersRepo.reset("externalIds");
|
||||||
|
PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
|
||||||
|
push.to(RefNames.REFS_EXTERNAL_IDS)
|
||||||
|
.assertErrorStatus("not allowed to update " + RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retryOnLockFailure() throws Exception {
|
||||||
|
Retryer<Void> retryer =
|
||||||
|
ExternalIdsUpdate.retryerBuilder()
|
||||||
|
.withBlockStrategy(
|
||||||
|
new BlockStrategy() {
|
||||||
|
@Override
|
||||||
|
public void block(long sleepTime) {
|
||||||
|
// Don't sleep in tests.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ExternalId.Key fooId = ExternalId.Key.create("foo", "foo");
|
||||||
|
ExternalId.Key barId = ExternalId.Key.create("bar", "bar");
|
||||||
|
|
||||||
|
final AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
|
||||||
|
ExternalIdsUpdate update =
|
||||||
|
new ExternalIdsUpdate(
|
||||||
|
repoManager,
|
||||||
|
allUsers,
|
||||||
|
serverIdent.get(),
|
||||||
|
serverIdent.get(),
|
||||||
|
() -> {
|
||||||
|
if (!doneBgUpdate.getAndSet(true)) {
|
||||||
|
try {
|
||||||
|
extIdsUpdate.create().insert(db, ExternalId.create(barId, admin.id));
|
||||||
|
} catch (IOException | ConfigInvalidException | OrmException e) {
|
||||||
|
// Ignore, the successful insertion of the external ID is asserted later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
retryer);
|
||||||
|
assertThat(doneBgUpdate.get()).isFalse();
|
||||||
|
update.insert(db, ExternalId.create(fooId, admin.id));
|
||||||
|
assertThat(doneBgUpdate.get()).isTrue();
|
||||||
|
|
||||||
|
assertThat(externalIds.get(fooId)).isNotNull();
|
||||||
|
assertThat(externalIds.get(barId)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failAfterRetryerGivesUp() throws Exception {
|
||||||
|
ExternalId.Key[] extIdsKeys = {
|
||||||
|
ExternalId.Key.create("foo", "foo"),
|
||||||
|
ExternalId.Key.create("bar", "bar"),
|
||||||
|
ExternalId.Key.create("baz", "baz")
|
||||||
|
};
|
||||||
|
final AtomicInteger bgCounter = new AtomicInteger(0);
|
||||||
|
ExternalIdsUpdate update =
|
||||||
|
new ExternalIdsUpdate(
|
||||||
|
repoManager,
|
||||||
|
allUsers,
|
||||||
|
serverIdent.get(),
|
||||||
|
serverIdent.get(),
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
extIdsUpdate
|
||||||
|
.create()
|
||||||
|
.insert(db, ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id));
|
||||||
|
} catch (IOException | ConfigInvalidException | OrmException e) {
|
||||||
|
// Ignore, the successful insertion of the external ID is asserted later
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RetryerBuilder.<Void>newBuilder()
|
||||||
|
.retryIfException(e -> e instanceof LockFailureException)
|
||||||
|
.withStopStrategy(StopStrategies.stopAfterAttempt(extIdsKeys.length))
|
||||||
|
.build());
|
||||||
|
assertThat(bgCounter.get()).isEqualTo(0);
|
||||||
|
try {
|
||||||
|
update.insert(db, ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id));
|
||||||
|
fail("expected LockFailureException");
|
||||||
|
} catch (LockFailureException e) {
|
||||||
|
// Ignore, expected
|
||||||
|
}
|
||||||
|
assertThat(bgCounter.get()).isEqualTo(extIdsKeys.length);
|
||||||
|
for (ExternalId.Key extIdKey : extIdsKeys) {
|
||||||
|
assertThat(externalIds.get(extIdKey)).isNotNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -88,7 +88,6 @@ public class ServerInfoIT extends AbstractDaemonTest {
|
|||||||
assertThat(i.auth.registerText).isNull();
|
assertThat(i.auth.registerText).isNull();
|
||||||
assertThat(i.auth.editFullNameUrl).isNull();
|
assertThat(i.auth.editFullNameUrl).isNull();
|
||||||
assertThat(i.auth.httpPasswordUrl).isNull();
|
assertThat(i.auth.httpPasswordUrl).isNull();
|
||||||
assertThat(i.auth.isGitBasicAuth).isNull();
|
|
||||||
|
|
||||||
// change
|
// change
|
||||||
assertThat(i.change.allowDrafts).isNull();
|
assertThat(i.change.allowDrafts).isNull();
|
||||||
@@ -163,7 +162,6 @@ public class ServerInfoIT extends AbstractDaemonTest {
|
|||||||
assertThat(i.auth.registerText).isNull();
|
assertThat(i.auth.registerText).isNull();
|
||||||
assertThat(i.auth.editFullNameUrl).isNull();
|
assertThat(i.auth.editFullNameUrl).isNull();
|
||||||
assertThat(i.auth.httpPasswordUrl).isNull();
|
assertThat(i.auth.httpPasswordUrl).isNull();
|
||||||
assertThat(i.auth.isGitBasicAuth).isNull();
|
|
||||||
|
|
||||||
// change
|
// change
|
||||||
assertThat(i.change.allowDrafts).isTrue();
|
assertThat(i.change.allowDrafts).isTrue();
|
||||||
|
@@ -31,6 +31,5 @@ public class AuthInfo {
|
|||||||
public String registerText;
|
public String registerText;
|
||||||
public String editFullNameUrl;
|
public String editFullNameUrl;
|
||||||
public String httpPasswordUrl;
|
public String httpPasswordUrl;
|
||||||
public Boolean isGitBasicAuth;
|
|
||||||
public GitBasicAuthPolicy gitBasicAuthPolicy;
|
public GitBasicAuthPolicy gitBasicAuthPolicy;
|
||||||
}
|
}
|
||||||
|
@@ -15,16 +15,16 @@
|
|||||||
package com.google.gerrit.gpg;
|
package com.google.gerrit.gpg;
|
||||||
|
|
||||||
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
|
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
import com.google.common.base.CharMatcher;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
import com.google.gerrit.common.PageLinks;
|
import com.google.gerrit.common.PageLinks;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
||||||
@@ -155,8 +155,7 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException {
|
private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException {
|
||||||
List<AccountState> accountStates =
|
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key));
|
||||||
accountQueryProvider.get().byExternalId(toExtIdKey(key).get());
|
|
||||||
if (accountStates.isEmpty()) {
|
if (accountStates.isEmpty()) {
|
||||||
return CheckResult.bad("Key is not associated with any users");
|
return CheckResult.bad("Key is not associated with any users");
|
||||||
}
|
}
|
||||||
@@ -202,11 +201,11 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
|
|||||||
private Set<String> getAllowedUserIds(IdentifiedUser user) {
|
private Set<String> getAllowedUserIds(IdentifiedUser user) {
|
||||||
Set<String> result = new HashSet<>();
|
Set<String> result = new HashSet<>();
|
||||||
result.addAll(user.getEmailAddresses());
|
result.addAll(user.getEmailAddresses());
|
||||||
for (AccountExternalId extId : user.state().getExternalIds()) {
|
for (ExternalId extId : user.state().getExternalIds()) {
|
||||||
if (extId.isScheme(SCHEME_GPGKEY)) {
|
if (extId.isScheme(SCHEME_GPGKEY)) {
|
||||||
continue; // Omit GPG keys.
|
continue; // Omit GPG keys.
|
||||||
}
|
}
|
||||||
result.add(extId.getExternalId());
|
result.add(extId.key().get());
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -248,8 +247,7 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
static AccountExternalId.Key toExtIdKey(PGPPublicKey key) {
|
static ExternalId.Key toExtIdKey(PGPPublicKey key) {
|
||||||
return new AccountExternalId.Key(
|
return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
|
||||||
SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,6 +33,7 @@ import java.io.IOException;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.eclipse.jgit.transport.PushCertificate;
|
import org.eclipse.jgit.transport.PushCertificate;
|
||||||
import org.eclipse.jgit.transport.PushCertificateParser;
|
import org.eclipse.jgit.transport.PushCertificateParser;
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ public class GpgApiAdapterImpl implements GpgApiAdapter {
|
|||||||
in.delete = delete;
|
in.delete = delete;
|
||||||
try {
|
try {
|
||||||
return postGpgKeys.apply(account, in);
|
return postGpgKeys.apply(account, in);
|
||||||
} catch (PGPException | OrmException | IOException e) {
|
} catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
|
||||||
throw new GpgException(e);
|
throw new GpgException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,7 @@ import com.google.inject.assistedinject.Assisted;
|
|||||||
import com.google.inject.assistedinject.AssistedInject;
|
import com.google.inject.assistedinject.AssistedInject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
|
||||||
public class GpgKeyApiImpl implements GpgKeyApi {
|
public class GpgKeyApiImpl implements GpgKeyApi {
|
||||||
public interface Factory {
|
public interface Factory {
|
||||||
@@ -55,7 +56,7 @@ public class GpgKeyApiImpl implements GpgKeyApi {
|
|||||||
public void delete() throws RestApiException {
|
public void delete() throws RestApiException {
|
||||||
try {
|
try {
|
||||||
delete.apply(rsrc, new DeleteGpgKey.Input());
|
delete.apply(rsrc, new DeleteGpgKey.Input());
|
||||||
} catch (PGPException | OrmException | IOException e) {
|
} catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
|
||||||
throw new RestApiException("Cannot delete GPG key", e);
|
throw new RestApiException("Cannot delete GPG key", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
package com.google.gerrit.gpg.server;
|
package com.google.gerrit.gpg.server;
|
||||||
|
|
||||||
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
|
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
@@ -22,17 +23,18 @@ import com.google.gerrit.extensions.restapi.Response;
|
|||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
import com.google.gerrit.gpg.PublicKeyStore;
|
import com.google.gerrit.gpg.PublicKeyStore;
|
||||||
import com.google.gerrit.gpg.server.DeleteGpgKey.Input;
|
import com.google.gerrit.gpg.server.DeleteGpgKey.Input;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.GerritPersonIdent;
|
import com.google.gerrit.server.GerritPersonIdent;
|
||||||
import com.google.gerrit.server.account.AccountCache;
|
import com.google.gerrit.server.account.AccountCache;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsUpdate;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.eclipse.jgit.lib.CommitBuilder;
|
import org.eclipse.jgit.lib.CommitBuilder;
|
||||||
import org.eclipse.jgit.lib.PersonIdent;
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
import org.eclipse.jgit.lib.RefUpdate;
|
import org.eclipse.jgit.lib.RefUpdate;
|
||||||
@@ -44,27 +46,34 @@ public class DeleteGpgKey implements RestModifyView<GpgKey, Input> {
|
|||||||
private final Provider<ReviewDb> db;
|
private final Provider<ReviewDb> db;
|
||||||
private final Provider<PublicKeyStore> storeProvider;
|
private final Provider<PublicKeyStore> storeProvider;
|
||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
|
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
DeleteGpgKey(
|
DeleteGpgKey(
|
||||||
@GerritPersonIdent Provider<PersonIdent> serverIdent,
|
@GerritPersonIdent Provider<PersonIdent> serverIdent,
|
||||||
Provider<ReviewDb> db,
|
Provider<ReviewDb> db,
|
||||||
Provider<PublicKeyStore> storeProvider,
|
Provider<PublicKeyStore> storeProvider,
|
||||||
AccountCache accountCache) {
|
AccountCache accountCache,
|
||||||
|
ExternalIdsUpdate.User externalIdsUpdateFactory) {
|
||||||
this.serverIdent = serverIdent;
|
this.serverIdent = serverIdent;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.storeProvider = storeProvider;
|
this.storeProvider = storeProvider;
|
||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
|
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<?> apply(GpgKey rsrc, Input input)
|
public Response<?> apply(GpgKey rsrc, Input input)
|
||||||
throws ResourceConflictException, PGPException, OrmException, IOException {
|
throws ResourceConflictException, PGPException, OrmException, IOException,
|
||||||
|
ConfigInvalidException {
|
||||||
PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
|
PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
|
||||||
AccountExternalId.Key extIdKey =
|
externalIdsUpdateFactory
|
||||||
new AccountExternalId.Key(
|
.create()
|
||||||
AccountExternalId.SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
|
.delete(
|
||||||
db.get().accountExternalIds().deleteKeys(Collections.singleton(extIdKey));
|
db.get(),
|
||||||
|
rsrc.getUser().getAccountId(),
|
||||||
|
ExternalId.Key.create(
|
||||||
|
SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
|
||||||
accountCache.evict(rsrc.getUser().getAccountId());
|
accountCache.evict(rsrc.getUser().getAccountId());
|
||||||
|
|
||||||
try (PublicKeyStore store = storeProvider.get()) {
|
try (PublicKeyStore store = storeProvider.get()) {
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.gpg.server;
|
package com.google.gerrit.gpg.server;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
@@ -37,10 +37,10 @@ import com.google.gerrit.gpg.GerritPublicKeyChecker;
|
|||||||
import com.google.gerrit.gpg.PublicKeyChecker;
|
import com.google.gerrit.gpg.PublicKeyChecker;
|
||||||
import com.google.gerrit.gpg.PublicKeyStore;
|
import com.google.gerrit.gpg.PublicKeyStore;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.account.AccountResource;
|
import com.google.gerrit.server.account.AccountResource;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
@@ -114,7 +114,7 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
|
|||||||
throw new ResourceNotFoundException(id);
|
throw new ResourceNotFoundException(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] parseFingerprint(String str, Iterable<AccountExternalId> existingExtIds)
|
static byte[] parseFingerprint(String str, Iterable<ExternalId> existingExtIds)
|
||||||
throws ResourceNotFoundException {
|
throws ResourceNotFoundException {
|
||||||
str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
|
str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
|
||||||
if ((str.length() != 8 && str.length() != 40)
|
if ((str.length() != 8 && str.length() != 40)
|
||||||
@@ -122,8 +122,8 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
|
|||||||
throw new ResourceNotFoundException(str);
|
throw new ResourceNotFoundException(str);
|
||||||
}
|
}
|
||||||
byte[] fp = null;
|
byte[] fp = null;
|
||||||
for (AccountExternalId extId : existingExtIds) {
|
for (ExternalId extId : existingExtIds) {
|
||||||
String fpStr = extId.getSchemeRest();
|
String fpStr = extId.key().id();
|
||||||
if (!fpStr.endsWith(str)) {
|
if (!fpStr.endsWith(str)) {
|
||||||
continue;
|
continue;
|
||||||
} else if (fp != null) {
|
} else if (fp != null) {
|
||||||
@@ -152,8 +152,8 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
|
|||||||
checkVisible(self, rsrc);
|
checkVisible(self, rsrc);
|
||||||
Map<String, GpgKeyInfo> keys = new HashMap<>();
|
Map<String, GpgKeyInfo> keys = new HashMap<>();
|
||||||
try (PublicKeyStore store = storeProvider.get()) {
|
try (PublicKeyStore store = storeProvider.get()) {
|
||||||
for (AccountExternalId extId : getGpgExtIds(rsrc)) {
|
for (ExternalId extId : getGpgExtIds(rsrc)) {
|
||||||
String fpStr = extId.getSchemeRest();
|
String fpStr = extId.key().id();
|
||||||
byte[] fp = BaseEncoding.base16().decode(fpStr);
|
byte[] fp = BaseEncoding.base16().decode(fpStr);
|
||||||
boolean found = false;
|
boolean found = false;
|
||||||
for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
|
for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
|
||||||
@@ -199,13 +199,14 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static FluentIterable<AccountExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId)
|
public static FluentIterable<ExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId)
|
||||||
throws OrmException {
|
throws OrmException {
|
||||||
return FluentIterable.from(db.accountExternalIds().byAccount(accountId))
|
return FluentIterable.from(
|
||||||
|
ExternalId.from(db.accountExternalIds().byAccount(accountId).toList()))
|
||||||
.filter(in -> in.isScheme(SCHEME_GPGKEY));
|
.filter(in -> in.isScheme(SCHEME_GPGKEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Iterable<AccountExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException {
|
private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException {
|
||||||
return getGpgExtIds(db.get(), rsrc.getUser().getAccountId());
|
return getGpgExtIds(db.get(), rsrc.getUser().getAccountId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,12 +16,13 @@ package com.google.gerrit.gpg.server;
|
|||||||
|
|
||||||
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
|
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
|
||||||
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
|
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
@@ -39,7 +40,6 @@ import com.google.gerrit.gpg.PublicKeyChecker;
|
|||||||
import com.google.gerrit.gpg.PublicKeyStore;
|
import com.google.gerrit.gpg.PublicKeyStore;
|
||||||
import com.google.gerrit.gpg.server.PostGpgKeys.Input;
|
import com.google.gerrit.gpg.server.PostGpgKeys.Input;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.GerritPersonIdent;
|
import com.google.gerrit.server.GerritPersonIdent;
|
||||||
@@ -47,6 +47,8 @@ import com.google.gerrit.server.IdentifiedUser;
|
|||||||
import com.google.gerrit.server.account.AccountCache;
|
import com.google.gerrit.server.account.AccountCache;
|
||||||
import com.google.gerrit.server.account.AccountResource;
|
import com.google.gerrit.server.account.AccountResource;
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsUpdate;
|
||||||
import com.google.gerrit.server.mail.send.AddKeySender;
|
import com.google.gerrit.server.mail.send.AddKeySender;
|
||||||
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
@@ -66,6 +68,7 @@ import org.bouncycastle.openpgp.PGPException;
|
|||||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
|
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.eclipse.jgit.lib.CommitBuilder;
|
import org.eclipse.jgit.lib.CommitBuilder;
|
||||||
import org.eclipse.jgit.lib.PersonIdent;
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
import org.eclipse.jgit.lib.RefUpdate;
|
import org.eclipse.jgit.lib.RefUpdate;
|
||||||
@@ -88,6 +91,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
|
|||||||
private final AddKeySender.Factory addKeyFactory;
|
private final AddKeySender.Factory addKeyFactory;
|
||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
private final Provider<InternalAccountQuery> accountQueryProvider;
|
private final Provider<InternalAccountQuery> accountQueryProvider;
|
||||||
|
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
PostGpgKeys(
|
PostGpgKeys(
|
||||||
@@ -98,7 +102,8 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
|
|||||||
GerritPublicKeyChecker.Factory checkerFactory,
|
GerritPublicKeyChecker.Factory checkerFactory,
|
||||||
AddKeySender.Factory addKeyFactory,
|
AddKeySender.Factory addKeyFactory,
|
||||||
AccountCache accountCache,
|
AccountCache accountCache,
|
||||||
Provider<InternalAccountQuery> accountQueryProvider) {
|
Provider<InternalAccountQuery> accountQueryProvider,
|
||||||
|
ExternalIdsUpdate.User externalIdsUpdateFactory) {
|
||||||
this.serverIdent = serverIdent;
|
this.serverIdent = serverIdent;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.self = self;
|
this.self = self;
|
||||||
@@ -107,48 +112,48 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
|
|||||||
this.addKeyFactory = addKeyFactory;
|
this.addKeyFactory = addKeyFactory;
|
||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
this.accountQueryProvider = accountQueryProvider;
|
this.accountQueryProvider = accountQueryProvider;
|
||||||
|
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
|
public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
|
||||||
throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
|
throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
|
||||||
PGPException, OrmException, IOException {
|
PGPException, OrmException, IOException, ConfigInvalidException {
|
||||||
GpgKeys.checkVisible(self, rsrc);
|
GpgKeys.checkVisible(self, rsrc);
|
||||||
|
|
||||||
List<AccountExternalId> existingExtIds =
|
Collection<ExternalId> existingExtIds =
|
||||||
GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
|
GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
|
||||||
|
|
||||||
try (PublicKeyStore store = storeProvider.get()) {
|
try (PublicKeyStore store = storeProvider.get()) {
|
||||||
Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
|
Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
|
||||||
List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
|
List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
|
||||||
List<AccountExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
|
List<ExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
|
||||||
|
|
||||||
for (PGPPublicKeyRing keyRing : newKeys) {
|
for (PGPPublicKeyRing keyRing : newKeys) {
|
||||||
PGPPublicKey key = keyRing.getPublicKey();
|
PGPPublicKey key = keyRing.getPublicKey();
|
||||||
AccountExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
|
ExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
|
||||||
Account account = getAccountByExternalId(extIdKey.get());
|
Account account = getAccountByExternalId(extIdKey);
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
if (!account.getId().equals(rsrc.getUser().getAccountId())) {
|
if (!account.getId().equals(rsrc.getUser().getAccountId())) {
|
||||||
throw new ResourceConflictException("GPG key already associated with another account");
|
throw new ResourceConflictException("GPG key already associated with another account");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newExtIds.add(new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
|
newExtIds.add(ExternalId.create(extIdKey, rsrc.getUser().getAccountId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storeKeys(rsrc, newKeys, toRemove);
|
storeKeys(rsrc, newKeys, toRemove);
|
||||||
if (!newExtIds.isEmpty()) {
|
|
||||||
db.get().accountExternalIds().insert(newExtIds);
|
List<ExternalId.Key> extIdKeysToRemove =
|
||||||
}
|
toRemove.stream().map(fp -> toExtIdKey(fp.get())).collect(toList());
|
||||||
db.get()
|
externalIdsUpdateFactory
|
||||||
.accountExternalIds()
|
.create()
|
||||||
.deleteKeys(Iterables.transform(toRemove, fp -> toExtIdKey(fp.get())));
|
.replace(db.get(), rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
|
||||||
accountCache.evict(rsrc.getUser().getAccountId());
|
accountCache.evict(rsrc.getUser().getAccountId());
|
||||||
return toJson(newKeys, toRemove, store, rsrc.getUser());
|
return toJson(newKeys, toRemove, store, rsrc.getUser());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Fingerprint> readKeysToRemove(Input input, List<AccountExternalId> existingExtIds) {
|
private Set<Fingerprint> readKeysToRemove(Input input, Collection<ExternalId> existingExtIds) {
|
||||||
if (input.delete == null || input.delete.isEmpty()) {
|
if (input.delete == null || input.delete.isEmpty()) {
|
||||||
return ImmutableSet.of();
|
return ImmutableSet.of();
|
||||||
}
|
}
|
||||||
@@ -243,13 +248,12 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccountExternalId.Key toExtIdKey(byte[] fp) {
|
private ExternalId.Key toExtIdKey(byte[] fp) {
|
||||||
return new AccountExternalId.Key(
|
return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
|
||||||
AccountExternalId.SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Account getAccountByExternalId(String externalId) throws OrmException {
|
private Account getAccountByExternalId(ExternalId.Key extIdKey) throws OrmException {
|
||||||
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(externalId);
|
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
|
||||||
|
|
||||||
if (accountStates.isEmpty()) {
|
if (accountStates.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -257,7 +261,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
|
|||||||
|
|
||||||
if (accountStates.size() > 1) {
|
if (accountStates.size() > 1) {
|
||||||
StringBuilder msg = new StringBuilder();
|
StringBuilder msg = new StringBuilder();
|
||||||
msg.append("GPG key ").append(externalId).append(" associated with multiple accounts: ");
|
msg.append("GPG key ").append(extIdKey.get()).append(" associated with multiple accounts: ");
|
||||||
Joiner.on(", ")
|
Joiner.on(", ")
|
||||||
.appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
|
.appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
|
||||||
log.error(msg.toString());
|
log.error(msg.toString());
|
||||||
|
@@ -23,7 +23,6 @@ import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
|
|||||||
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
|
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
|
||||||
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD;
|
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD;
|
||||||
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE;
|
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE;
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
|
|
||||||
import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
|
import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
|
||||||
import static org.eclipse.jgit.lib.RefUpdate.Result.FORCED;
|
import static org.eclipse.jgit.lib.RefUpdate.Result.FORCED;
|
||||||
import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
|
import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
|
||||||
@@ -34,13 +33,14 @@ import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
|
|||||||
import com.google.gerrit.gpg.testutil.TestKey;
|
import com.google.gerrit.gpg.testutil.TestKey;
|
||||||
import com.google.gerrit.lifecycle.LifecycleManager;
|
import com.google.gerrit.lifecycle.LifecycleManager;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.account.AccountCache;
|
import com.google.gerrit.server.account.AccountCache;
|
||||||
import com.google.gerrit.server.account.AccountManager;
|
import com.google.gerrit.server.account.AccountManager;
|
||||||
import com.google.gerrit.server.account.AuthRequest;
|
import com.google.gerrit.server.account.AuthRequest;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsUpdate;
|
||||||
import com.google.gerrit.server.schema.SchemaCreator;
|
import com.google.gerrit.server.schema.SchemaCreator;
|
||||||
import com.google.gerrit.server.util.RequestContext;
|
import com.google.gerrit.server.util.RequestContext;
|
||||||
import com.google.gerrit.server.util.ThreadLocalRequestContext;
|
import com.google.gerrit.server.util.ThreadLocalRequestContext;
|
||||||
@@ -55,7 +55,6 @@ import com.google.inject.util.Providers;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
@@ -86,6 +85,8 @@ public class GerritPublicKeyCheckerTest {
|
|||||||
|
|
||||||
@Inject private ThreadLocalRequestContext requestContext;
|
@Inject private ThreadLocalRequestContext requestContext;
|
||||||
|
|
||||||
|
@Inject private ExternalIdsUpdate.Server externalIdsUpdateFactory;
|
||||||
|
|
||||||
private LifecycleManager lifecycle;
|
private LifecycleManager lifecycle;
|
||||||
private ReviewDb db;
|
private ReviewDb db;
|
||||||
private Account.Id userId;
|
private Account.Id userId;
|
||||||
@@ -221,7 +222,8 @@ public class GerritPublicKeyCheckerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noExternalIds() throws Exception {
|
public void noExternalIds() throws Exception {
|
||||||
db.accountExternalIds().delete(db.accountExternalIds().byAccount(user.getAccountId()));
|
ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
|
||||||
|
externalIdsUpdate.deleteAll(db, user.getAccountId());
|
||||||
reloadUser();
|
reloadUser();
|
||||||
|
|
||||||
TestKey key = validKeyWithSecondUserId();
|
TestKey key = validKeyWithSecondUserId();
|
||||||
@@ -234,11 +236,8 @@ public class GerritPublicKeyCheckerTest {
|
|||||||
checker = checkerFactory.create().setStore(store).disableTrust();
|
checker = checkerFactory.create().setStore(store).disableTrust();
|
||||||
assertProblems(
|
assertProblems(
|
||||||
checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users");
|
checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users");
|
||||||
|
externalIdsUpdate.insert(
|
||||||
db.accountExternalIds()
|
db, ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId()));
|
||||||
.insert(
|
|
||||||
Collections.singleton(
|
|
||||||
new AccountExternalId(user.getAccountId(), toExtIdKey(key.getPublicKey()))));
|
|
||||||
reloadUser();
|
reloadUser();
|
||||||
assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user");
|
assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user");
|
||||||
}
|
}
|
||||||
@@ -389,18 +388,15 @@ public class GerritPublicKeyCheckerTest {
|
|||||||
|
|
||||||
private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception {
|
private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception {
|
||||||
Account.Id id = user.getAccountId();
|
Account.Id id = user.getAccountId();
|
||||||
List<AccountExternalId> newExtIds = new ArrayList<>(2);
|
List<ExternalId> newExtIds = new ArrayList<>(2);
|
||||||
newExtIds.add(new AccountExternalId(id, toExtIdKey(kr.getPublicKey())));
|
newExtIds.add(ExternalId.create(toExtIdKey(kr.getPublicKey()), id));
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
String userId = (String) Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null);
|
String userId = (String) Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null);
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
String email = PushCertificateIdent.parse(userId).getEmailAddress();
|
String email = PushCertificateIdent.parse(userId).getEmailAddress();
|
||||||
assertThat(email).contains("@");
|
assertThat(email).contains("@");
|
||||||
AccountExternalId mailto =
|
newExtIds.add(ExternalId.createEmail(id, email));
|
||||||
new AccountExternalId(id, new AccountExternalId.Key(SCHEME_MAILTO, email));
|
|
||||||
mailto.setEmailAddress(email);
|
|
||||||
newExtIds.add(mailto);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
store.add(kr);
|
store.add(kr);
|
||||||
@@ -410,7 +406,7 @@ public class GerritPublicKeyCheckerTest {
|
|||||||
cb.setCommitter(ident);
|
cb.setCommitter(ident);
|
||||||
assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
|
assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
|
||||||
|
|
||||||
db.accountExternalIds().insert(newExtIds);
|
externalIdsUpdateFactory.create().insert(db, newExtIds);
|
||||||
accountCache.evict(user.getAccountId());
|
accountCache.evict(user.getAccountId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,12 +430,9 @@ public class GerritPublicKeyCheckerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addExternalId(String scheme, String id, String email) throws Exception {
|
private void addExternalId(String scheme, String id, String email) throws Exception {
|
||||||
AccountExternalId extId =
|
externalIdsUpdateFactory
|
||||||
new AccountExternalId(user.getAccountId(), new AccountExternalId.Key(scheme, id));
|
.create()
|
||||||
if (email != null) {
|
.insert(db, ExternalId.createWithEmail(scheme, id, user.getAccountId(), email));
|
||||||
extId.setEmailAddress(email);
|
|
||||||
}
|
|
||||||
db.accountExternalIds().insert(Collections.singleton(extId));
|
|
||||||
reloadUser();
|
reloadUser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -22,12 +22,12 @@ import com.google.gerrit.common.data.HostPageData;
|
|||||||
import com.google.gerrit.httpd.WebSessionManager.Key;
|
import com.google.gerrit.httpd.WebSessionManager.Key;
|
||||||
import com.google.gerrit.httpd.WebSessionManager.Val;
|
import com.google.gerrit.httpd.WebSessionManager.Val;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.AccessPath;
|
import com.google.gerrit.server.AccessPath;
|
||||||
import com.google.gerrit.server.AnonymousUser;
|
import com.google.gerrit.server.AnonymousUser;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.account.AuthResult;
|
import com.google.gerrit.server.account.AuthResult;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.servlet.RequestScoped;
|
import com.google.inject.servlet.RequestScoped;
|
||||||
@@ -132,7 +132,7 @@ public abstract class CacheBasedWebSession implements WebSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccountExternalId.Key getLastLoginExternalId() {
|
public ExternalId.Key getLastLoginExternalId() {
|
||||||
return val != null ? val.getExternalId() : null;
|
return val != null ? val.getExternalId() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,9 +149,9 @@ public abstract class CacheBasedWebSession implements WebSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void login(final AuthResult res, final boolean rememberMe) {
|
public void login(AuthResult res, boolean rememberMe) {
|
||||||
final Account.Id id = res.getAccountId();
|
Account.Id id = res.getAccountId();
|
||||||
final AccountExternalId.Key identity = res.getExternalId();
|
ExternalId.Key identity = res.getExternalId();
|
||||||
|
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
manager.destroy(key);
|
manager.destroy(key);
|
||||||
|
@@ -42,14 +42,10 @@ public class GitOverHttpModule extends ServletModule {
|
|||||||
Class<? extends Filter> authFilter;
|
Class<? extends Filter> authFilter;
|
||||||
if (authConfig.isTrustContainerAuth()) {
|
if (authConfig.isTrustContainerAuth()) {
|
||||||
authFilter = ContainerAuthFilter.class;
|
authFilter = ContainerAuthFilter.class;
|
||||||
} else if (authConfig.isGitBasicAuth()) {
|
} else if (authConfig.getAuthType() == OAUTH) {
|
||||||
if (authConfig.getAuthType() == OAUTH) {
|
authFilter = ProjectOAuthFilter.class;
|
||||||
authFilter = ProjectOAuthFilter.class;
|
|
||||||
} else {
|
|
||||||
authFilter = ProjectBasicAuthFilter.class;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
authFilter = ProjectDigestFilter.class;
|
authFilter = ProjectBasicAuthFilter.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHttpEnabled()) {
|
if (isHttpEnabled()) {
|
||||||
|
@@ -140,7 +140,7 @@ class ProjectBasicAuthFilter implements Filter {
|
|||||||
GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
|
GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
|
||||||
if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
|
if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
|
||||||
|| gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
|
|| gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
|
||||||
if (passwordMatchesTheUserGeneratedOne(who, username, password)) {
|
if (who.checkPassword(password, username)) {
|
||||||
return succeedAuthentication(who);
|
return succeedAuthentication(who);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ class ProjectBasicAuthFilter implements Filter {
|
|||||||
setUserIdentified(whoAuthResult.getAccountId());
|
setUserIdentified(whoAuthResult.getAccountId());
|
||||||
return true;
|
return true;
|
||||||
} catch (NoSuchUserException e) {
|
} catch (NoSuchUserException e) {
|
||||||
if (password.equals(who.getPassword(who.getUserName()))) {
|
if (who.checkPassword(password, who.getUserName())) {
|
||||||
return succeedAuthentication(who);
|
return succeedAuthentication(who);
|
||||||
}
|
}
|
||||||
log.warn("Authentication failed for " + username, e);
|
log.warn("Authentication failed for " + username, e);
|
||||||
@@ -193,12 +193,6 @@ class ProjectBasicAuthFilter implements Filter {
|
|||||||
ws.setAccessPathOk(AccessPath.REST_API, true);
|
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) {
|
private String encoding(HttpServletRequest req) {
|
||||||
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -16,10 +16,10 @@ package com.google.gerrit.httpd;
|
|||||||
|
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.AccessPath;
|
import com.google.gerrit.server.AccessPath;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.account.AuthResult;
|
import com.google.gerrit.server.account.AuthResult;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
|
||||||
public interface WebSession {
|
public interface WebSession {
|
||||||
boolean isSignedIn();
|
boolean isSignedIn();
|
||||||
@@ -29,7 +29,7 @@ public interface WebSession {
|
|||||||
|
|
||||||
boolean isValidXGerritAuth(String keyIn);
|
boolean isValidXGerritAuth(String keyIn);
|
||||||
|
|
||||||
AccountExternalId.Key getLastLoginExternalId();
|
ExternalId.Key getLastLoginExternalId();
|
||||||
|
|
||||||
CurrentUser getUser();
|
CurrentUser getUser();
|
||||||
|
|
||||||
|
@@ -30,7 +30,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
|
|||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.config.ConfigUtil;
|
import com.google.gerrit.server.config.ConfigUtil;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
@@ -98,18 +98,18 @@ public class WebSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Val createVal(final Key key, final Val val) {
|
Val createVal(Key key, Val val) {
|
||||||
final Account.Id who = val.getAccountId();
|
Account.Id who = val.getAccountId();
|
||||||
final boolean remember = val.isPersistentCookie();
|
boolean remember = val.isPersistentCookie();
|
||||||
final AccountExternalId.Key lastLogin = val.getExternalId();
|
ExternalId.Key lastLogin = val.getExternalId();
|
||||||
return createVal(key, who, remember, lastLogin, val.sessionId, val.auth);
|
return createVal(key, who, remember, lastLogin, val.sessionId, val.auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
Val createVal(
|
Val createVal(
|
||||||
final Key key,
|
Key key,
|
||||||
final Account.Id who,
|
Account.Id who,
|
||||||
final boolean remember,
|
boolean remember,
|
||||||
final AccountExternalId.Key lastLogin,
|
ExternalId.Key lastLogin,
|
||||||
String sid,
|
String sid,
|
||||||
String auth) {
|
String auth) {
|
||||||
// Refresh the cookie every hour or when it is half-expired.
|
// Refresh the cookie every hour or when it is half-expired.
|
||||||
@@ -191,19 +191,19 @@ public class WebSessionManager {
|
|||||||
private transient Account.Id accountId;
|
private transient Account.Id accountId;
|
||||||
private transient long refreshCookieAt;
|
private transient long refreshCookieAt;
|
||||||
private transient boolean persistentCookie;
|
private transient boolean persistentCookie;
|
||||||
private transient AccountExternalId.Key externalId;
|
private transient ExternalId.Key externalId;
|
||||||
private transient long expiresAt;
|
private transient long expiresAt;
|
||||||
private transient String sessionId;
|
private transient String sessionId;
|
||||||
private transient String auth;
|
private transient String auth;
|
||||||
|
|
||||||
Val(
|
Val(
|
||||||
final Account.Id accountId,
|
Account.Id accountId,
|
||||||
final long refreshCookieAt,
|
long refreshCookieAt,
|
||||||
final boolean persistentCookie,
|
boolean persistentCookie,
|
||||||
final AccountExternalId.Key externalId,
|
ExternalId.Key externalId,
|
||||||
final long expiresAt,
|
long expiresAt,
|
||||||
final String sessionId,
|
String sessionId,
|
||||||
final String auth) {
|
String auth) {
|
||||||
this.accountId = accountId;
|
this.accountId = accountId;
|
||||||
this.refreshCookieAt = refreshCookieAt;
|
this.refreshCookieAt = refreshCookieAt;
|
||||||
this.persistentCookie = persistentCookie;
|
this.persistentCookie = persistentCookie;
|
||||||
@@ -221,7 +221,7 @@ public class WebSessionManager {
|
|||||||
return accountId;
|
return accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountExternalId.Key getExternalId() {
|
ExternalId.Key getExternalId() {
|
||||||
return externalId;
|
return externalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ public class WebSessionManager {
|
|||||||
|
|
||||||
if (externalId != null) {
|
if (externalId != null) {
|
||||||
writeVarInt32(out, 4);
|
writeVarInt32(out, 4);
|
||||||
writeString(out, externalId.get());
|
writeString(out, externalId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
@@ -289,7 +289,7 @@ public class WebSessionManager {
|
|||||||
persistentCookie = readVarInt32(in) != 0;
|
persistentCookie = readVarInt32(in) != 0;
|
||||||
continue;
|
continue;
|
||||||
case 4:
|
case 4:
|
||||||
externalId = new AccountExternalId.Key(readString(in));
|
externalId = ExternalId.Key.parse(readString(in));
|
||||||
continue;
|
continue;
|
||||||
case 5:
|
case 5:
|
||||||
sessionId = readString(in);
|
sessionId = readString(in);
|
||||||
|
@@ -14,7 +14,8 @@
|
|||||||
|
|
||||||
package com.google.gerrit.httpd.auth.become;
|
package com.google.gerrit.httpd.auth.become;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID;
|
||||||
|
|
||||||
import com.google.gerrit.common.PageLinks;
|
import com.google.gerrit.common.PageLinks;
|
||||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||||
@@ -23,13 +24,13 @@ import com.google.gerrit.httpd.LoginUrlToken;
|
|||||||
import com.google.gerrit.httpd.WebSession;
|
import com.google.gerrit.httpd.WebSession;
|
||||||
import com.google.gerrit.httpd.template.SiteHeaderFooter;
|
import com.google.gerrit.httpd.template.SiteHeaderFooter;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.account.AccountException;
|
import com.google.gerrit.server.account.AccountException;
|
||||||
import com.google.gerrit.server.account.AccountManager;
|
import com.google.gerrit.server.account.AccountManager;
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
import com.google.gerrit.server.account.AuthRequest;
|
import com.google.gerrit.server.account.AuthRequest;
|
||||||
import com.google.gerrit.server.account.AuthResult;
|
import com.google.gerrit.server.account.AuthResult;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
||||||
import com.google.gwtexpui.server.CacheHeaders;
|
import com.google.gwtexpui.server.CacheHeaders;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
@@ -179,17 +180,16 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult auth(final AccountExternalId account) {
|
private AuthResult auth(Account.Id account) {
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
return new AuthResult(account.getAccountId(), null, false);
|
return new AuthResult(account, null, false);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult byUserName(final String userName) {
|
private AuthResult byUserName(final String userName) {
|
||||||
try {
|
try {
|
||||||
AccountExternalId.Key extKey = new AccountExternalId.Key(SCHEME_USERNAME, userName);
|
List<AccountState> accountStates = accountQuery.byExternalId(SCHEME_USERNAME, userName);
|
||||||
List<AccountState> accountStates = accountQuery.byExternalId(extKey.get());
|
|
||||||
if (accountStates.isEmpty()) {
|
if (accountStates.isEmpty()) {
|
||||||
getServletContext().log("No accounts with username " + userName + " found");
|
getServletContext().log("No accounts with username " + userName + " found");
|
||||||
return null;
|
return null;
|
||||||
@@ -198,7 +198,7 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
|
|||||||
getServletContext().log("Multiple accounts with username " + userName + " found");
|
getServletContext().log("Multiple accounts with username " + userName + " found");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return auth(new AccountExternalId(accountStates.get(0).getAccount().getId(), extKey));
|
return auth(accountStates.get(0).getAccount().getId());
|
||||||
} catch (OrmException e) {
|
} catch (OrmException e) {
|
||||||
getServletContext().log("cannot query account index", e);
|
getServletContext().log("cannot query account index", e);
|
||||||
return null;
|
return null;
|
||||||
@@ -231,9 +231,9 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult create() throws IOException {
|
private AuthResult create() throws IOException {
|
||||||
String fakeId = AccountExternalId.SCHEME_UUID + UUID.randomUUID();
|
|
||||||
try {
|
try {
|
||||||
return accountManager.authenticate(new AuthRequest(fakeId));
|
return accountManager.authenticate(
|
||||||
|
new AuthRequest(ExternalId.Key.create(SCHEME_UUID, UUID.randomUUID().toString())));
|
||||||
} catch (AccountException e) {
|
} catch (AccountException e) {
|
||||||
getServletContext().log("cannot create new account", e);
|
getServletContext().log("cannot create new account", e);
|
||||||
return null;
|
return null;
|
||||||
|
@@ -17,7 +17,7 @@ package com.google.gerrit.httpd.auth.container;
|
|||||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||||
import static com.google.common.base.Strings.emptyToNull;
|
import static com.google.common.base.Strings.emptyToNull;
|
||||||
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
|
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
|
||||||
import static java.nio.charset.StandardCharsets.ISO_8859_1;
|
import static java.nio.charset.StandardCharsets.ISO_8859_1;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ import com.google.gerrit.httpd.HtmlDomUtil;
|
|||||||
import com.google.gerrit.httpd.RemoteUserUtil;
|
import com.google.gerrit.httpd.RemoteUserUtil;
|
||||||
import com.google.gerrit.httpd.WebSession;
|
import com.google.gerrit.httpd.WebSession;
|
||||||
import com.google.gerrit.httpd.raw.HostPageServlet;
|
import com.google.gerrit.httpd.raw.HostPageServlet;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
import com.google.gwtexpui.server.CacheHeaders;
|
import com.google.gwtexpui.server.CacheHeaders;
|
||||||
import com.google.gwtjsonrpc.server.RPCServletUtils;
|
import com.google.gwtjsonrpc.server.RPCServletUtils;
|
||||||
@@ -127,8 +127,8 @@ class HttpAuthFilter implements Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean correctUser(String user, WebSession session) {
|
private static boolean correctUser(String user, WebSession session) {
|
||||||
AccountExternalId.Key id = session.getLastLoginExternalId();
|
ExternalId.Key id = session.getLastLoginExternalId();
|
||||||
return id != null && id.equals(new AccountExternalId.Key(SCHEME_GERRIT, user));
|
return id != null && id.equals(ExternalId.Key.create(SCHEME_GERRIT, user));
|
||||||
}
|
}
|
||||||
|
|
||||||
String getRemoteUser(HttpServletRequest req) {
|
String getRemoteUser(HttpServletRequest req) {
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.httpd.auth.container;
|
package com.google.gerrit.httpd.auth.container;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
import com.google.gerrit.common.PageLinks;
|
import com.google.gerrit.common.PageLinks;
|
||||||
@@ -23,11 +23,11 @@ import com.google.gerrit.httpd.CanonicalWebUrl;
|
|||||||
import com.google.gerrit.httpd.HtmlDomUtil;
|
import com.google.gerrit.httpd.HtmlDomUtil;
|
||||||
import com.google.gerrit.httpd.LoginUrlToken;
|
import com.google.gerrit.httpd.LoginUrlToken;
|
||||||
import com.google.gerrit.httpd.WebSession;
|
import com.google.gerrit.httpd.WebSession;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.account.AccountException;
|
import com.google.gerrit.server.account.AccountException;
|
||||||
import com.google.gerrit.server.account.AccountManager;
|
import com.google.gerrit.server.account.AccountManager;
|
||||||
import com.google.gerrit.server.account.AuthRequest;
|
import com.google.gerrit.server.account.AuthRequest;
|
||||||
import com.google.gerrit.server.account.AuthResult;
|
import com.google.gerrit.server.account.AuthResult;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
import com.google.gwtexpui.server.CacheHeaders;
|
import com.google.gwtexpui.server.CacheHeaders;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
@@ -39,6 +39,7 @@ import javax.servlet.ServletOutputStream;
|
|||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
@@ -127,7 +128,7 @@ class HttpLoginServlet extends HttpServlet {
|
|||||||
try {
|
try {
|
||||||
log.debug("Associating external identity \"{}\" to user \"{}\"", remoteExternalId, user);
|
log.debug("Associating external identity \"{}\" to user \"{}\"", remoteExternalId, user);
|
||||||
updateRemoteExternalId(arsp, remoteExternalId);
|
updateRemoteExternalId(arsp, remoteExternalId);
|
||||||
} catch (AccountException | OrmException e) {
|
} catch (AccountException | OrmException | ConfigInvalidException e) {
|
||||||
log.error(
|
log.error(
|
||||||
"Unable to associate external identity \""
|
"Unable to associate external identity \""
|
||||||
+ remoteExternalId
|
+ remoteExternalId
|
||||||
@@ -156,12 +157,10 @@ class HttpLoginServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken)
|
private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken)
|
||||||
throws AccountException, OrmException, IOException {
|
throws AccountException, OrmException, IOException, ConfigInvalidException {
|
||||||
AccountExternalId remoteAuthExtId =
|
|
||||||
new AccountExternalId(
|
|
||||||
arsp.getAccountId(), new AccountExternalId.Key(SCHEME_EXTERNAL, remoteAuthToken));
|
|
||||||
accountManager.updateLink(
|
accountManager.updateLink(
|
||||||
arsp.getAccountId(), new AuthRequest(remoteAuthExtId.getExternalId()));
|
arsp.getAccountId(),
|
||||||
|
new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, remoteAuthToken)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void replace(Document doc, String name, String value) {
|
private void replace(Document doc, String name, String value) {
|
||||||
|
@@ -22,6 +22,7 @@ java_library(
|
|||||||
"//lib/commons:codec",
|
"//lib/commons:codec",
|
||||||
"//lib/guice",
|
"//lib/guice",
|
||||||
"//lib/guice:guice-servlet",
|
"//lib/guice:guice-servlet",
|
||||||
|
"//lib/jgit/org.eclipse.jgit:jgit",
|
||||||
"//lib/log:api",
|
"//lib/log:api",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -31,6 +31,7 @@ import com.google.gerrit.server.account.AccountException;
|
|||||||
import com.google.gerrit.server.account.AccountManager;
|
import com.google.gerrit.server.account.AccountManager;
|
||||||
import com.google.gerrit.server.account.AuthRequest;
|
import com.google.gerrit.server.account.AuthRequest;
|
||||||
import com.google.gerrit.server.account.AuthResult;
|
import com.google.gerrit.server.account.AuthResult;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
|
import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
@@ -44,6 +45,7 @@ import javax.servlet.ServletRequest;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -124,7 +126,7 @@ class OAuthSession {
|
|||||||
|
|
||||||
private void authenticateAndRedirect(
|
private void authenticateAndRedirect(
|
||||||
HttpServletRequest req, HttpServletResponse rsp, OAuthToken token) throws IOException {
|
HttpServletRequest req, HttpServletResponse rsp, OAuthToken token) throws IOException {
|
||||||
AuthRequest areq = new AuthRequest(user.getExternalId());
|
AuthRequest areq = new AuthRequest(ExternalId.Key.parse(user.getExternalId()));
|
||||||
AuthResult arsp;
|
AuthResult arsp;
|
||||||
try {
|
try {
|
||||||
String claimedIdentifier = user.getClaimedIdentity();
|
String claimedIdentifier = user.getClaimedIdentity();
|
||||||
@@ -190,7 +192,7 @@ class OAuthSession {
|
|||||||
log.info("OAuth2: linking claimed identity to {}", claimedId.get().toString());
|
log.info("OAuth2: linking claimed identity to {}", claimedId.get().toString());
|
||||||
try {
|
try {
|
||||||
accountManager.link(claimedId.get(), req);
|
accountManager.link(claimedId.get(), req);
|
||||||
} catch (OrmException e) {
|
} catch (OrmException | ConfigInvalidException e) {
|
||||||
log.error(
|
log.error(
|
||||||
"Cannot link: "
|
"Cannot link: "
|
||||||
+ user.getExternalId()
|
+ user.getExternalId()
|
||||||
@@ -210,7 +212,7 @@ class OAuthSession {
|
|||||||
throws AccountException, IOException {
|
throws AccountException, IOException {
|
||||||
try {
|
try {
|
||||||
accountManager.link(identifiedUser.get().getAccountId(), areq);
|
accountManager.link(identifiedUser.get().getAccountId(), areq);
|
||||||
} catch (OrmException e) {
|
} catch (OrmException | ConfigInvalidException e) {
|
||||||
log.error(
|
log.error(
|
||||||
"Cannot link: "
|
"Cannot link: "
|
||||||
+ user.getExternalId()
|
+ user.getExternalId()
|
||||||
|
@@ -31,6 +31,7 @@ import com.google.gerrit.server.IdentifiedUser;
|
|||||||
import com.google.gerrit.server.account.AccountException;
|
import com.google.gerrit.server.account.AccountException;
|
||||||
import com.google.gerrit.server.account.AccountManager;
|
import com.google.gerrit.server.account.AccountManager;
|
||||||
import com.google.gerrit.server.account.AuthResult;
|
import com.google.gerrit.server.account.AuthResult;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
@@ -43,6 +44,7 @@ import javax.servlet.ServletRequest;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -116,7 +118,8 @@ class OAuthSessionOverOpenID {
|
|||||||
private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp)
|
private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
com.google.gerrit.server.account.AuthRequest areq =
|
com.google.gerrit.server.account.AuthRequest areq =
|
||||||
new com.google.gerrit.server.account.AuthRequest(user.getExternalId());
|
new com.google.gerrit.server.account.AuthRequest(
|
||||||
|
ExternalId.Key.parse(user.getExternalId()));
|
||||||
AuthResult arsp = null;
|
AuthResult arsp = null;
|
||||||
try {
|
try {
|
||||||
String claimedIdentifier = user.getClaimedIdentity();
|
String claimedIdentifier = user.getClaimedIdentity();
|
||||||
@@ -167,7 +170,7 @@ class OAuthSessionOverOpenID {
|
|||||||
log.debug("Claimed account already exists: link to it.");
|
log.debug("Claimed account already exists: link to it.");
|
||||||
try {
|
try {
|
||||||
accountManager.link(claimedId.get(), areq);
|
accountManager.link(claimedId.get(), areq);
|
||||||
} catch (OrmException e) {
|
} catch (OrmException | ConfigInvalidException e) {
|
||||||
log.error(
|
log.error(
|
||||||
"Cannot link: "
|
"Cannot link: "
|
||||||
+ user.getExternalId()
|
+ user.getExternalId()
|
||||||
@@ -186,7 +189,7 @@ class OAuthSessionOverOpenID {
|
|||||||
try {
|
try {
|
||||||
log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(), accountId);
|
log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(), accountId);
|
||||||
accountManager.link(accountId, areq);
|
accountManager.link(accountId, areq);
|
||||||
} catch (OrmException e) {
|
} catch (OrmException | ConfigInvalidException e) {
|
||||||
log.error("Cannot link: " + user.getExternalId() + " to user identity: " + accountId);
|
log.error("Cannot link: " + user.getExternalId() + " to user identity: " + accountId);
|
||||||
rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
|
rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||||
return;
|
return;
|
||||||
|
@@ -26,6 +26,7 @@ import com.google.gerrit.server.IdentifiedUser;
|
|||||||
import com.google.gerrit.server.UrlEncoded;
|
import com.google.gerrit.server.UrlEncoded;
|
||||||
import com.google.gerrit.server.account.AccountException;
|
import com.google.gerrit.server.account.AccountException;
|
||||||
import com.google.gerrit.server.account.AccountManager;
|
import com.google.gerrit.server.account.AccountManager;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
|
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
import com.google.gerrit.server.config.ConfigUtil;
|
import com.google.gerrit.server.config.ConfigUtil;
|
||||||
@@ -314,7 +315,7 @@ class OpenIdServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final com.google.gerrit.server.account.AuthRequest areq =
|
final com.google.gerrit.server.account.AuthRequest areq =
|
||||||
new com.google.gerrit.server.account.AuthRequest(openidIdentifier);
|
new com.google.gerrit.server.account.AuthRequest(ExternalId.Key.parse(openidIdentifier));
|
||||||
|
|
||||||
if (sregRsp != null) {
|
if (sregRsp != null) {
|
||||||
areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
|
areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
|
||||||
@@ -369,7 +370,7 @@ class OpenIdServiceImpl {
|
|||||||
// link between the two, so set one up if not present.
|
// link between the two, so set one up if not present.
|
||||||
//
|
//
|
||||||
Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier);
|
Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier);
|
||||||
Optional<Account.Id> actualId = accountManager.lookup(areq.getExternalId());
|
Optional<Account.Id> actualId = accountManager.lookup(areq.getExternalIdKey().get());
|
||||||
|
|
||||||
if (claimedId.isPresent() && actualId.isPresent()) {
|
if (claimedId.isPresent() && actualId.isPresent()) {
|
||||||
if (claimedId.get().equals(actualId.get())) {
|
if (claimedId.get().equals(actualId.get())) {
|
||||||
@@ -388,7 +389,7 @@ class OpenIdServiceImpl {
|
|||||||
+ " Delgate ID: "
|
+ " Delgate ID: "
|
||||||
+ actualId.get()
|
+ actualId.get()
|
||||||
+ " is "
|
+ " is "
|
||||||
+ areq.getExternalId());
|
+ areq.getExternalIdKey());
|
||||||
cancelWithError(req, rsp, "Contact site administrator");
|
cancelWithError(req, rsp, "Contact site administrator");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -398,7 +399,8 @@ class OpenIdServiceImpl {
|
|||||||
// was missing due to a bug in Gerrit. Link the claimed.
|
// was missing due to a bug in Gerrit. Link the claimed.
|
||||||
//
|
//
|
||||||
final com.google.gerrit.server.account.AuthRequest linkReq =
|
final com.google.gerrit.server.account.AuthRequest linkReq =
|
||||||
new com.google.gerrit.server.account.AuthRequest(claimedIdentifier);
|
new com.google.gerrit.server.account.AuthRequest(
|
||||||
|
ExternalId.Key.parse(claimedIdentifier));
|
||||||
linkReq.setDisplayName(areq.getDisplayName());
|
linkReq.setDisplayName(areq.getDisplayName());
|
||||||
linkReq.setEmailAddress(areq.getEmailAddress());
|
linkReq.setEmailAddress(areq.getEmailAddress());
|
||||||
accountManager.link(actualId.get(), linkReq);
|
accountManager.link(actualId.get(), linkReq);
|
||||||
@@ -434,7 +436,8 @@ class OpenIdServiceImpl {
|
|||||||
webSession.get().login(arsp, remember);
|
webSession.get().login(arsp, remember);
|
||||||
if (arsp.isNew() && claimedIdentifier != null) {
|
if (arsp.isNew() && claimedIdentifier != null) {
|
||||||
final com.google.gerrit.server.account.AuthRequest linkReq =
|
final com.google.gerrit.server.account.AuthRequest linkReq =
|
||||||
new com.google.gerrit.server.account.AuthRequest(claimedIdentifier);
|
new com.google.gerrit.server.account.AuthRequest(
|
||||||
|
ExternalId.Key.parse(claimedIdentifier));
|
||||||
linkReq.setDisplayName(areq.getDisplayName());
|
linkReq.setDisplayName(areq.getDisplayName());
|
||||||
linkReq.setEmailAddress(areq.getEmailAddress());
|
linkReq.setEmailAddress(areq.getEmailAddress());
|
||||||
accountManager.link(arsp.getAccountId(), linkReq);
|
accountManager.link(arsp.getAccountId(), linkReq);
|
||||||
|
@@ -14,115 +14,67 @@
|
|||||||
|
|
||||||
package com.google.gerrit.pgm;
|
package com.google.gerrit.pgm;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
|
||||||
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
|
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
|
||||||
|
|
||||||
import com.google.gerrit.lifecycle.LifecycleManager;
|
import com.google.gerrit.lifecycle.LifecycleManager;
|
||||||
import com.google.gerrit.pgm.util.SiteProgram;
|
import com.google.gerrit.pgm.util.SiteProgram;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsBatchUpdate;
|
||||||
import com.google.gerrit.server.schema.SchemaVersionCheck;
|
import com.google.gerrit.server.schema.SchemaVersionCheck;
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.gwtorm.server.SchemaFactory;
|
import com.google.gwtorm.server.SchemaFactory;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import org.eclipse.jgit.lib.TextProgressMonitor;
|
import org.eclipse.jgit.lib.TextProgressMonitor;
|
||||||
import org.kohsuke.args4j.Option;
|
|
||||||
|
|
||||||
/** Converts the local username for all accounts to lower case */
|
/** Converts the local username for all accounts to lower case */
|
||||||
public class LocalUsernamesToLowerCase extends SiteProgram {
|
public class LocalUsernamesToLowerCase extends SiteProgram {
|
||||||
@Option(name = "--threads", usage = "Number of concurrent threads to run")
|
|
||||||
private int threads = 2;
|
|
||||||
|
|
||||||
private final LifecycleManager manager = new LifecycleManager();
|
private final LifecycleManager manager = new LifecycleManager();
|
||||||
private final TextProgressMonitor monitor = new TextProgressMonitor();
|
private final TextProgressMonitor monitor = new TextProgressMonitor();
|
||||||
private List<AccountExternalId> todo;
|
|
||||||
|
|
||||||
private Injector dbInjector;
|
|
||||||
|
|
||||||
@Inject private SchemaFactory<ReviewDb> database;
|
@Inject private SchemaFactory<ReviewDb> database;
|
||||||
|
|
||||||
|
@Inject private ExternalIdsBatchUpdate externalIdsBatchUpdate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int run() throws Exception {
|
public int run() throws Exception {
|
||||||
if (threads <= 0) {
|
Injector dbInjector = createDbInjector(MULTI_USER);
|
||||||
threads = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbInjector = createDbInjector(MULTI_USER);
|
|
||||||
manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module()));
|
manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module()));
|
||||||
manager.start();
|
manager.start();
|
||||||
dbInjector.injectMembers(this);
|
dbInjector.injectMembers(this);
|
||||||
|
|
||||||
try (ReviewDb db = database.open()) {
|
try (ReviewDb db = database.open()) {
|
||||||
todo = db.accountExternalIds().all().toList();
|
Collection<ExternalId> todo = ExternalId.from(db.accountExternalIds().all().toList());
|
||||||
synchronized (monitor) {
|
monitor.beginTask("Converting local usernames", todo.size());
|
||||||
monitor.beginTask("Converting local usernames", todo.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Worker> workers = new ArrayList<>(threads);
|
for (ExternalId extId : todo) {
|
||||||
for (int tid = 0; tid < threads; tid++) {
|
convertLocalUserToLowerCase(extId);
|
||||||
Worker t = new Worker();
|
monitor.update(1);
|
||||||
t.start();
|
}
|
||||||
workers.add(t);
|
|
||||||
}
|
externalIdsBatchUpdate.commit(db, "Convert local usernames to lower case");
|
||||||
for (Worker t : workers) {
|
|
||||||
t.join();
|
|
||||||
}
|
|
||||||
synchronized (monitor) {
|
|
||||||
monitor.endTask();
|
|
||||||
}
|
}
|
||||||
|
monitor.endTask();
|
||||||
manager.stop();
|
manager.stop();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void convertLocalUserToLowerCase(final ReviewDb db, final AccountExternalId extId) {
|
private void convertLocalUserToLowerCase(ExternalId extId) {
|
||||||
if (extId.isScheme(AccountExternalId.SCHEME_GERRIT)) {
|
if (extId.isScheme(SCHEME_GERRIT)) {
|
||||||
final String localUser = extId.getSchemeRest();
|
String localUser = extId.key().id();
|
||||||
final String localUserLowerCase = localUser.toLowerCase(Locale.US);
|
String localUserLowerCase = localUser.toLowerCase(Locale.US);
|
||||||
if (!localUser.equals(localUserLowerCase)) {
|
if (!localUser.equals(localUserLowerCase)) {
|
||||||
final AccountExternalId.Key extIdKeyLowerCase =
|
ExternalId extIdLowerCase =
|
||||||
new AccountExternalId.Key(AccountExternalId.SCHEME_GERRIT, localUserLowerCase);
|
ExternalId.create(
|
||||||
final AccountExternalId extIdLowerCase =
|
SCHEME_GERRIT,
|
||||||
new AccountExternalId(extId.getAccountId(), extIdKeyLowerCase);
|
localUserLowerCase,
|
||||||
try {
|
extId.accountId(),
|
||||||
db.accountExternalIds().insert(Collections.singleton(extIdLowerCase));
|
extId.email(),
|
||||||
db.accountExternalIds().delete(Collections.singleton(extId));
|
extId.password());
|
||||||
} catch (OrmException error) {
|
externalIdsBatchUpdate.replace(extId, extIdLowerCase);
|
||||||
System.err.println("ERR " + error.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AccountExternalId next() {
|
|
||||||
synchronized (todo) {
|
|
||||||
if (todo.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return todo.remove(todo.size() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Worker extends Thread {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try (ReviewDb db = database.open()) {
|
|
||||||
for (; ; ) {
|
|
||||||
final AccountExternalId extId = next();
|
|
||||||
if (extId == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
convertLocalUserToLowerCase(db, extId);
|
|
||||||
synchronized (monitor) {
|
|
||||||
monitor.update(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (OrmException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (C) 2016 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.pgm.init;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
|
||||||
|
|
||||||
|
import com.google.gerrit.pgm.init.api.InitFlags;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.GerritPersonIdentProvider;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
import com.google.gerrit.server.account.ExternalIds;
|
||||||
|
import com.google.gerrit.server.account.ExternalIdsUpdate;
|
||||||
|
import com.google.gerrit.server.config.SitePaths;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.internal.storage.file.FileRepository;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.ObjectInserter;
|
||||||
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
|
||||||
|
import org.eclipse.jgit.notes.NoteMap;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.eclipse.jgit.util.FS;
|
||||||
|
|
||||||
|
public class ExternalIdsOnInit {
|
||||||
|
private final InitFlags flags;
|
||||||
|
private final SitePaths site;
|
||||||
|
private final String allUsers;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ExternalIdsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) {
|
||||||
|
this.flags = flags;
|
||||||
|
this.site = site;
|
||||||
|
this.allUsers = allUsers.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void insert(ReviewDb db, String commitMessage, Collection<ExternalId> extIds)
|
||||||
|
throws OrmException, IOException, ConfigInvalidException {
|
||||||
|
db.accountExternalIds().insert(toAccountExternalIds(extIds));
|
||||||
|
|
||||||
|
File path = getPath();
|
||||||
|
if (path != null) {
|
||||||
|
try (Repository repo = new FileRepository(path);
|
||||||
|
RevWalk rw = new RevWalk(repo);
|
||||||
|
ObjectInserter ins = repo.newObjectInserter()) {
|
||||||
|
ObjectId rev = ExternalIds.readRevision(repo);
|
||||||
|
|
||||||
|
NoteMap noteMap = ExternalIds.readNoteMap(rw, rev);
|
||||||
|
for (ExternalId extId : extIds) {
|
||||||
|
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
|
||||||
|
}
|
||||||
|
|
||||||
|
PersonIdent serverIdent = new GerritPersonIdentProvider(flags.cfg).get();
|
||||||
|
ExternalIdsUpdate.commit(
|
||||||
|
repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getPath() {
|
||||||
|
Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
|
||||||
|
if (basePath == null) {
|
||||||
|
throw new IllegalStateException("gerrit.basePath must be configured");
|
||||||
|
}
|
||||||
|
return FileKey.resolve(basePath.resolve(allUsers).toFile(), FS.DETECTED);
|
||||||
|
}
|
||||||
|
}
|
@@ -23,13 +23,13 @@ import com.google.gerrit.pgm.init.api.ConsoleUI;
|
|||||||
import com.google.gerrit.pgm.init.api.InitFlags;
|
import com.google.gerrit.pgm.init.api.InitFlags;
|
||||||
import com.google.gerrit.pgm.init.api.InitStep;
|
import com.google.gerrit.pgm.init.api.InitStep;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroupName;
|
import com.google.gerrit.reviewdb.client.AccountGroupName;
|
||||||
import com.google.gerrit.reviewdb.client.AccountSshKey;
|
import com.google.gerrit.reviewdb.client.AccountSshKey;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.index.account.AccountIndex;
|
import com.google.gerrit.server.index.account.AccountIndex;
|
||||||
import com.google.gerrit.server.index.account.AccountIndexCollection;
|
import com.google.gerrit.server.index.account.AccountIndexCollection;
|
||||||
import com.google.gwtorm.server.SchemaFactory;
|
import com.google.gwtorm.server.SchemaFactory;
|
||||||
@@ -48,15 +48,20 @@ public class InitAdminUser implements InitStep {
|
|||||||
private final ConsoleUI ui;
|
private final ConsoleUI ui;
|
||||||
private final InitFlags flags;
|
private final InitFlags flags;
|
||||||
private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
|
private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
|
||||||
|
private final ExternalIdsOnInit externalIds;
|
||||||
private SchemaFactory<ReviewDb> dbFactory;
|
private SchemaFactory<ReviewDb> dbFactory;
|
||||||
private AccountIndexCollection indexCollection;
|
private AccountIndexCollection indexCollection;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
InitAdminUser(
|
InitAdminUser(
|
||||||
InitFlags flags, ConsoleUI ui, VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory) {
|
InitFlags flags,
|
||||||
|
ConsoleUI ui,
|
||||||
|
VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
|
||||||
|
ExternalIdsOnInit externalIds) {
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
this.ui = ui;
|
this.ui = ui;
|
||||||
this.authorizedKeysFactory = authorizedKeysFactory;
|
this.authorizedKeysFactory = authorizedKeysFactory;
|
||||||
|
this.externalIds = externalIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -90,24 +95,13 @@ public class InitAdminUser implements InitStep {
|
|||||||
AccountSshKey sshKey = readSshKey(id);
|
AccountSshKey sshKey = readSshKey(id);
|
||||||
String email = readEmail(sshKey);
|
String email = readEmail(sshKey);
|
||||||
|
|
||||||
List<AccountExternalId> extIds = new ArrayList<>(2);
|
List<ExternalId> extIds = new ArrayList<>(2);
|
||||||
AccountExternalId extUser =
|
extIds.add(ExternalId.createUsername(username, id, httpPassword));
|
||||||
new AccountExternalId(
|
|
||||||
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
|
|
||||||
if (!Strings.isNullOrEmpty(httpPassword)) {
|
|
||||||
extUser.setPassword(httpPassword);
|
|
||||||
}
|
|
||||||
extIds.add(extUser);
|
|
||||||
db.accountExternalIds().insert(Collections.singleton(extUser));
|
|
||||||
|
|
||||||
if (email != null) {
|
if (email != null) {
|
||||||
AccountExternalId extMailto =
|
extIds.add(ExternalId.createEmail(id, email));
|
||||||
new AccountExternalId(
|
|
||||||
id, new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email));
|
|
||||||
extMailto.setEmailAddress(email);
|
|
||||||
extIds.add(extMailto);
|
|
||||||
db.accountExternalIds().insert(Collections.singleton(extMailto));
|
|
||||||
}
|
}
|
||||||
|
externalIds.insert(db, "Add external IDs for initial admin user", extIds);
|
||||||
|
|
||||||
Account a = new Account(id, TimeUtil.nowTs());
|
Account a = new Account(id, TimeUtil.nowTs());
|
||||||
a.setFullName(name);
|
a.setFullName(name);
|
||||||
@@ -123,7 +117,7 @@ public class InitAdminUser implements InitStep {
|
|||||||
if (sshKey != null) {
|
if (sshKey != null) {
|
||||||
VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
|
VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
|
||||||
authorizedKeys.addKey(sshKey.getSshPublicKey());
|
authorizedKeys.addKey(sshKey.getSshPublicKey());
|
||||||
authorizedKeys.save("Added SSH key for initial admin user\n");
|
authorizedKeys.save("Add SSH key for initial admin user\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountGroup adminGroup = db.accountGroups().get(adminGroupName.getId());
|
AccountGroup adminGroup = db.accountGroups().get(adminGroupName.getId());
|
||||||
|
@@ -25,16 +25,16 @@ import java.sql.Timestamp;
|
|||||||
/**
|
/**
|
||||||
* Information about a single user.
|
* Information about a single user.
|
||||||
*
|
*
|
||||||
* <p>A user may have multiple identities they can use to login to Gerrit (see {@link
|
* <p>A user may have multiple identities they can use to login to Gerrit (see ExternalId), but in
|
||||||
* AccountExternalId}), but in such cases they always map back to a single Account entity.
|
* such cases they always map back to a single Account entity.
|
||||||
*
|
*
|
||||||
* <p>Entities "owned" by an Account (that is, their primary key contains the {@link Account.Id} key
|
* <p>Entities "owned" by an Account (that is, their primary key contains the {@link Account.Id} key
|
||||||
* as part of their key structure):
|
* as part of their key structure):
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link AccountExternalId}: OpenID identities and email addresses known to be registered to
|
* <li>ExternalId: OpenID identities and email addresses known to be registered to this user.
|
||||||
* this user. Multiple records can exist when the user has more than one public identity, such
|
* Multiple records can exist when the user has more than one public identity, such as a work
|
||||||
* as a work and a personal email address.
|
* and a personal email address.
|
||||||
* <li>{@link AccountGroupMember}: membership of the user in a specific human managed {@link
|
* <li>{@link AccountGroupMember}: membership of the user in a specific human managed {@link
|
||||||
* AccountGroup}. Multiple records can exist when the user is a member of more than one group.
|
* AccountGroup}. Multiple records can exist when the user is a member of more than one group.
|
||||||
* <li>{@link AccountSshKey}: user's public SSH keys, for authentication through the internal SSH
|
* <li>{@link AccountSshKey}: user's public SSH keys, for authentication through the internal SSH
|
||||||
|
@@ -17,6 +17,7 @@ package com.google.gerrit.reviewdb.client;
|
|||||||
import com.google.gerrit.extensions.client.AuthType;
|
import com.google.gerrit.extensions.client.AuthType;
|
||||||
import com.google.gwtorm.client.Column;
|
import com.google.gwtorm.client.Column;
|
||||||
import com.google.gwtorm.client.StringKey;
|
import com.google.gwtorm.client.StringKey;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/** Association of an external account identifier to a local {@link Account}. */
|
/** Association of an external account identifier to a local {@link Account}. */
|
||||||
public final class AccountExternalId {
|
public final class AccountExternalId {
|
||||||
@@ -87,6 +88,8 @@ public final class AccountExternalId {
|
|||||||
@Column(id = 3, notNull = false)
|
@Column(id = 3, notNull = false)
|
||||||
protected String emailAddress;
|
protected String emailAddress;
|
||||||
|
|
||||||
|
// Encoded version of the hashed and salted password, to be interpreted by the
|
||||||
|
// {@link HashedPassword} class.
|
||||||
@Column(id = 4, notNull = false)
|
@Column(id = 4, notNull = false)
|
||||||
protected String password;
|
protected String password;
|
||||||
|
|
||||||
@@ -140,12 +143,12 @@ public final class AccountExternalId {
|
|||||||
return null != scheme ? getExternalId().substring(scheme.length() + 1) : null;
|
return null != scheme ? getExternalId().substring(scheme.length() + 1) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPassword() {
|
public void setPassword(String hashed) {
|
||||||
return password;
|
password = hashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPassword(String p) {
|
public String getPassword() {
|
||||||
password = p;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTrusted() {
|
public boolean isTrusted() {
|
||||||
@@ -163,4 +166,21 @@ public final class AccountExternalId {
|
|||||||
public void setCanDelete(final boolean t) {
|
public void setCanDelete(final boolean t) {
|
||||||
canDelete = t;
|
canDelete = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o instanceof AccountExternalId) {
|
||||||
|
AccountExternalId extId = (AccountExternalId) o;
|
||||||
|
return Objects.equals(key, extId.key)
|
||||||
|
&& Objects.equals(accountId, extId.accountId)
|
||||||
|
&& Objects.equals(emailAddress, extId.emailAddress)
|
||||||
|
&& Objects.equals(password, extId.password);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(key, accountId, emailAddress, password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,9 @@ public class RefNames {
|
|||||||
/** Configuration settings for a project {@code refs/meta/config} */
|
/** Configuration settings for a project {@code refs/meta/config} */
|
||||||
public static final String REFS_CONFIG = "refs/meta/config";
|
public static final String REFS_CONFIG = "refs/meta/config";
|
||||||
|
|
||||||
|
/** Note tree listing external IDs */
|
||||||
|
public static final String REFS_EXTERNAL_IDS = "refs/meta/external-ids";
|
||||||
|
|
||||||
/** Preference settings for a user {@code refs/users} */
|
/** Preference settings for a user {@code refs/users} */
|
||||||
public static final String REFS_USERS = "refs/users/";
|
public static final String REFS_USERS = "refs/users/";
|
||||||
|
|
||||||
|
@@ -230,9 +230,11 @@ junit_tests(
|
|||||||
"//lib:guava",
|
"//lib:guava",
|
||||||
"//lib:guava-retrying",
|
"//lib:guava-retrying",
|
||||||
"//lib:protobuf",
|
"//lib:protobuf",
|
||||||
|
"//lib/bouncycastle:bcprov",
|
||||||
"//lib/dropwizard:dropwizard-core",
|
"//lib/dropwizard:dropwizard-core",
|
||||||
"//lib/guice:guice-assistedinject",
|
"//lib/guice:guice-assistedinject",
|
||||||
"//lib/prolog:runtime",
|
"//lib/prolog:runtime",
|
||||||
|
"//lib/commons:codec",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -16,8 +16,8 @@ package com.google.gerrit.server;
|
|||||||
|
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.account.CapabilityControl;
|
import com.google.gerrit.server.account.CapabilityControl;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.account.GroupMembership;
|
import com.google.gerrit.server.account.GroupMembership;
|
||||||
import com.google.inject.servlet.RequestScoped;
|
import com.google.inject.servlet.RequestScoped;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -44,7 +44,7 @@ public abstract class CurrentUser {
|
|||||||
private AccessPath accessPath = AccessPath.UNKNOWN;
|
private AccessPath accessPath = AccessPath.UNKNOWN;
|
||||||
|
|
||||||
private CapabilityControl capabilities;
|
private CapabilityControl capabilities;
|
||||||
private PropertyKey<AccountExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create();
|
private PropertyKey<ExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create();
|
||||||
|
|
||||||
protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) {
|
protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) {
|
||||||
this.capabilityControlFactory = capabilityControlFactory;
|
this.capabilityControlFactory = capabilityControlFactory;
|
||||||
@@ -151,11 +151,11 @@ public abstract class CurrentUser {
|
|||||||
*/
|
*/
|
||||||
public <T> void put(PropertyKey<T> key, @Nullable T value) {}
|
public <T> void put(PropertyKey<T> key, @Nullable T value) {}
|
||||||
|
|
||||||
public void setLastLoginExternalIdKey(AccountExternalId.Key externalIdKey) {
|
public void setLastLoginExternalIdKey(ExternalId.Key externalIdKey) {
|
||||||
put(lastLoginExternalIdPropertyKey, externalIdKey);
|
put(lastLoginExternalIdPropertyKey, externalIdKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountExternalId.Key getLastLoginExternalIdKey() {
|
public ExternalId.Key getLastLoginExternalIdKey() {
|
||||||
return get(lastLoginExternalIdPropertyKey);
|
return get(lastLoginExternalIdPropertyKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,6 @@ package com.google.gerrit.server.account;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.gerrit.extensions.client.AccountFieldName;
|
import com.google.gerrit.extensions.client.AccountFieldName;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.mail.send.EmailSender;
|
import com.google.gerrit.server.mail.send.EmailSender;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
@@ -53,8 +52,8 @@ public abstract class AbstractRealm implements Realm {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasEmailAddress(IdentifiedUser user, String email) {
|
public boolean hasEmailAddress(IdentifiedUser user, String email) {
|
||||||
for (AccountExternalId ext : user.state().getExternalIds()) {
|
for (ExternalId ext : user.state().getExternalIds()) {
|
||||||
if (email != null && email.equalsIgnoreCase(ext.getEmailAddress())) {
|
if (email != null && email.equalsIgnoreCase(ext.email())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,11 +62,11 @@ public abstract class AbstractRealm implements Realm {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getEmailAddresses(IdentifiedUser user) {
|
public Set<String> getEmailAddresses(IdentifiedUser user) {
|
||||||
Collection<AccountExternalId> ids = user.state().getExternalIds();
|
Collection<ExternalId> ids = user.state().getExternalIds();
|
||||||
Set<String> emails = Sets.newHashSetWithExpectedSize(ids.size());
|
Set<String> emails = Sets.newHashSetWithExpectedSize(ids.size());
|
||||||
for (AccountExternalId ext : ids) {
|
for (ExternalId ext : ids) {
|
||||||
if (!Strings.isNullOrEmpty(ext.getEmailAddress())) {
|
if (!Strings.isNullOrEmpty(ext.email())) {
|
||||||
emails.add(ext.getEmailAddress());
|
emails.add(ext.email());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return emails;
|
return emails;
|
||||||
|
@@ -18,7 +18,6 @@ import com.google.common.cache.CacheLoader;
|
|||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.cache.CacheModule;
|
import com.google.gerrit.server.cache.CacheModule;
|
||||||
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
||||||
@@ -94,12 +93,15 @@ public class AccountByEmailCacheImpl implements AccountByEmailCache {
|
|||||||
for (Account a : db.accounts().byPreferredEmail(email)) {
|
for (Account a : db.accounts().byPreferredEmail(email)) {
|
||||||
r.add(a.getId());
|
r.add(a.getId());
|
||||||
}
|
}
|
||||||
for (AccountState accountState :
|
for (AccountState accountState : accountQueryProvider.get().byEmailPrefix(email)) {
|
||||||
accountQueryProvider
|
if (accountState
|
||||||
.get()
|
.getExternalIds()
|
||||||
.byExternalId(
|
.stream()
|
||||||
(new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email)).get())) {
|
.filter(e -> email.equals(e.email()))
|
||||||
r.add(accountState.getAccount().getId());
|
.findAny()
|
||||||
|
.isPresent()) {
|
||||||
|
r.add(accountState.getAccount().getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ImmutableSet.copyOf(r);
|
return ImmutableSet.copyOf(r);
|
||||||
}
|
}
|
||||||
|
@@ -14,13 +14,14 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
|
||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
@@ -38,7 +39,6 @@ import com.google.inject.Singleton;
|
|||||||
import com.google.inject.TypeLiteral;
|
import com.google.inject.TypeLiteral;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -138,9 +138,9 @@ public class AccountCacheImpl implements AccountCache {
|
|||||||
private static AccountState missing(Account.Id accountId) {
|
private static AccountState missing(Account.Id accountId) {
|
||||||
Account account = new Account(accountId, TimeUtil.nowTs());
|
Account account = new Account(accountId, TimeUtil.nowTs());
|
||||||
account.setActive(false);
|
account.setActive(false);
|
||||||
Collection<AccountExternalId> ids = Collections.emptySet();
|
|
||||||
Set<AccountGroup.UUID> anon = ImmutableSet.of();
|
Set<AccountGroup.UUID> anon = ImmutableSet.of();
|
||||||
return new AccountState(account, anon, ids, new HashMap<ProjectWatchKey, Set<NotifyType>>());
|
return new AccountState(
|
||||||
|
account, anon, Collections.emptySet(), new HashMap<ProjectWatchKey, Set<NotifyType>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
|
static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
|
||||||
@@ -184,8 +184,8 @@ public class AccountCacheImpl implements AccountCache {
|
|||||||
return missing(who);
|
return missing(who);
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection<AccountExternalId> externalIds =
|
Set<ExternalId> externalIds =
|
||||||
Collections.unmodifiableCollection(db.accountExternalIds().byAccount(who).toList());
|
ExternalId.from(db.accountExternalIds().byAccount(who).toList());
|
||||||
|
|
||||||
Set<AccountGroup.UUID> internalGroups = new HashSet<>();
|
Set<AccountGroup.UUID> internalGroups = new HashSet<>();
|
||||||
for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
|
for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
|
||||||
@@ -219,11 +219,8 @@ public class AccountCacheImpl implements AccountCache {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Account.Id> load(String username) throws Exception {
|
public Optional<Account.Id> load(String username) throws Exception {
|
||||||
AccountExternalId.Key key =
|
AccountState accountState =
|
||||||
new AccountExternalId.Key( //
|
accountQueryProvider.get().oneByExternalId(SCHEME_USERNAME, username);
|
||||||
AccountExternalId.SCHEME_USERNAME, //
|
|
||||||
username);
|
|
||||||
AccountState accountState = accountQueryProvider.get().oneByExternalId(key.get());
|
|
||||||
return Optional.ofNullable(accountState).map(s -> s.getAccount().getId());
|
return Optional.ofNullable(accountState).map(s -> s.getAccount().getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.toSet;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.gerrit.audit.AuditService;
|
import com.google.gerrit.audit.AuditService;
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
@@ -23,7 +25,6 @@ import com.google.gerrit.common.data.Permission;
|
|||||||
import com.google.gerrit.common.errors.NameAlreadyUsedException;
|
import com.google.gerrit.common.errors.NameAlreadyUsedException;
|
||||||
import com.google.gerrit.extensions.client.AccountFieldName;
|
import com.google.gerrit.extensions.client.AccountFieldName;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
@@ -31,17 +32,16 @@ import com.google.gerrit.server.IdentifiedUser;
|
|||||||
import com.google.gerrit.server.project.ProjectCache;
|
import com.google.gerrit.server.project.ProjectCache;
|
||||||
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.gwtorm.server.ResultSet;
|
|
||||||
import com.google.gwtorm.server.SchemaFactory;
|
import com.google.gwtorm.server.SchemaFactory;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ public class AccountManager {
|
|||||||
private final AtomicBoolean awaitsFirstAccountCheck;
|
private final AtomicBoolean awaitsFirstAccountCheck;
|
||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
private final Provider<InternalAccountQuery> accountQueryProvider;
|
private final Provider<InternalAccountQuery> accountQueryProvider;
|
||||||
|
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AccountManager(
|
AccountManager(
|
||||||
@@ -71,7 +72,8 @@ public class AccountManager {
|
|||||||
ChangeUserName.Factory changeUserNameFactory,
|
ChangeUserName.Factory changeUserNameFactory,
|
||||||
ProjectCache projectCache,
|
ProjectCache projectCache,
|
||||||
AuditService auditService,
|
AuditService auditService,
|
||||||
Provider<InternalAccountQuery> accountQueryProvider) {
|
Provider<InternalAccountQuery> accountQueryProvider,
|
||||||
|
ExternalIdsUpdate.Server externalIdsUpdateFactory) {
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
this.byIdCache = byIdCache;
|
this.byIdCache = byIdCache;
|
||||||
this.byEmailCache = byEmailCache;
|
this.byEmailCache = byEmailCache;
|
||||||
@@ -82,6 +84,7 @@ public class AccountManager {
|
|||||||
this.awaitsFirstAccountCheck = new AtomicBoolean(true);
|
this.awaitsFirstAccountCheck = new AtomicBoolean(true);
|
||||||
this.auditService = auditService;
|
this.auditService = auditService;
|
||||||
this.accountQueryProvider = accountQueryProvider;
|
this.accountQueryProvider = accountQueryProvider;
|
||||||
|
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return user identified by this external identity string */
|
/** @return user identified by this external identity string */
|
||||||
@@ -108,8 +111,7 @@ public class AccountManager {
|
|||||||
who = realm.authenticate(who);
|
who = realm.authenticate(who);
|
||||||
try {
|
try {
|
||||||
try (ReviewDb db = schema.open()) {
|
try (ReviewDb db = schema.open()) {
|
||||||
AccountExternalId.Key key = id(who);
|
ExternalId id = findExternalId(who.getExternalIdKey());
|
||||||
AccountExternalId id = getAccountExternalId(key);
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
// New account, automatically create and return.
|
// New account, automatically create and return.
|
||||||
//
|
//
|
||||||
@@ -117,25 +119,25 @@ public class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Account exists
|
// Account exists
|
||||||
Account act = byIdCache.get(id.getAccountId()).getAccount();
|
Account act = byIdCache.get(id.accountId()).getAccount();
|
||||||
if (!act.isActive()) {
|
if (!act.isActive()) {
|
||||||
throw new AccountException("Authentication error, account inactive");
|
throw new AccountException("Authentication error, account inactive");
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the identity to the caller.
|
// return the identity to the caller.
|
||||||
update(db, who, id);
|
update(db, who, id);
|
||||||
return new AuthResult(id.getAccountId(), key, false);
|
return new AuthResult(id.accountId(), who.getExternalIdKey(), false);
|
||||||
}
|
}
|
||||||
} catch (OrmException e) {
|
} catch (OrmException | ConfigInvalidException e) {
|
||||||
throw new AccountException("Authentication error", e);
|
throw new AccountException("Authentication error", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccountExternalId getAccountExternalId(AccountExternalId.Key key) throws OrmException {
|
private ExternalId findExternalId(ExternalId.Key key) throws OrmException {
|
||||||
AccountState accountState = accountQueryProvider.get().oneByExternalId(key.get());
|
AccountState accountState = accountQueryProvider.get().oneByExternalId(key);
|
||||||
if (accountState != null) {
|
if (accountState != null) {
|
||||||
for (AccountExternalId extId : accountState.getExternalIds()) {
|
for (ExternalId extId : accountState.getExternalIds()) {
|
||||||
if (extId.getKey().equals(key)) {
|
if (extId.key().equals(key)) {
|
||||||
return extId;
|
return extId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,24 +145,28 @@ public class AccountManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update(ReviewDb db, AuthRequest who, AccountExternalId extId)
|
private void update(ReviewDb db, AuthRequest who, ExternalId extId)
|
||||||
throws OrmException, IOException {
|
throws OrmException, IOException, ConfigInvalidException {
|
||||||
IdentifiedUser user = userFactory.create(extId.getAccountId());
|
IdentifiedUser user = userFactory.create(extId.accountId());
|
||||||
Account toUpdate = null;
|
Account toUpdate = null;
|
||||||
|
|
||||||
// If the email address was modified by the authentication provider,
|
// If the email address was modified by the authentication provider,
|
||||||
// update our records to match the changed email.
|
// update our records to match the changed email.
|
||||||
//
|
//
|
||||||
String newEmail = who.getEmailAddress();
|
String newEmail = who.getEmailAddress();
|
||||||
String oldEmail = extId.getEmailAddress();
|
String oldEmail = extId.email();
|
||||||
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
||||||
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
|
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
|
||||||
toUpdate = load(toUpdate, user.getAccountId(), db);
|
toUpdate = load(toUpdate, user.getAccountId(), db);
|
||||||
toUpdate.setPreferredEmail(newEmail);
|
toUpdate.setPreferredEmail(newEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
extId.setEmailAddress(newEmail);
|
externalIdsUpdateFactory
|
||||||
db.accountExternalIds().update(Collections.singleton(extId));
|
.create()
|
||||||
|
.replace(
|
||||||
|
db,
|
||||||
|
extId,
|
||||||
|
ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
|
if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
|
||||||
@@ -206,14 +212,14 @@ public class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult create(ReviewDb db, AuthRequest who)
|
private AuthResult create(ReviewDb db, AuthRequest who)
|
||||||
throws OrmException, AccountException, IOException {
|
throws OrmException, AccountException, IOException, ConfigInvalidException {
|
||||||
Account.Id newId = new Account.Id(db.nextAccountId());
|
Account.Id newId = new Account.Id(db.nextAccountId());
|
||||||
Account account = new Account(newId, TimeUtil.nowTs());
|
Account account = new Account(newId, TimeUtil.nowTs());
|
||||||
AccountExternalId extId = createId(newId, who);
|
|
||||||
|
|
||||||
extId.setEmailAddress(who.getEmailAddress());
|
ExternalId extId =
|
||||||
|
ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
|
||||||
account.setFullName(who.getDisplayName());
|
account.setFullName(who.getDisplayName());
|
||||||
account.setPreferredEmail(extId.getEmailAddress());
|
account.setPreferredEmail(extId.email());
|
||||||
|
|
||||||
boolean isFirstAccount =
|
boolean isFirstAccount =
|
||||||
awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty();
|
awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty();
|
||||||
@@ -221,18 +227,19 @@ public class AccountManager {
|
|||||||
try {
|
try {
|
||||||
db.accounts().upsert(Collections.singleton(account));
|
db.accounts().upsert(Collections.singleton(account));
|
||||||
|
|
||||||
AccountExternalId existingExtId = db.accountExternalIds().get(extId.getKey());
|
ExternalId existingExtId =
|
||||||
if (existingExtId != null && !existingExtId.getAccountId().equals(extId.getAccountId())) {
|
ExternalId.from(db.accountExternalIds().get(extId.key().asAccountExternalIdKey()));
|
||||||
|
if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) {
|
||||||
// external ID is assigned to another account, do not overwrite
|
// external ID is assigned to another account, do not overwrite
|
||||||
db.accounts().delete(Collections.singleton(account));
|
db.accounts().delete(Collections.singleton(account));
|
||||||
throw new AccountException(
|
throw new AccountException(
|
||||||
"Cannot assign external ID \""
|
"Cannot assign external ID \""
|
||||||
+ extId.getExternalId()
|
+ extId.key().get()
|
||||||
+ "\" to account "
|
+ "\" to account "
|
||||||
+ newId
|
+ newId
|
||||||
+ "; external ID already in use.");
|
+ "; external ID already in use.");
|
||||||
}
|
}
|
||||||
db.accountExternalIds().upsert(Collections.singleton(extId));
|
externalIdsUpdateFactory.create().upsert(db, extId);
|
||||||
} finally {
|
} finally {
|
||||||
// If adding the account failed, it may be that it actually was the
|
// If adding the account failed, it may be that it actually was the
|
||||||
// first account. So we reset the 'check for first account'-guard, as
|
// first account. So we reset the 'check for first account'-guard, as
|
||||||
@@ -291,7 +298,7 @@ public class AccountManager {
|
|||||||
byEmailCache.evict(account.getPreferredEmail());
|
byEmailCache.evict(account.getPreferredEmail());
|
||||||
byIdCache.evict(account.getId());
|
byIdCache.evict(account.getId());
|
||||||
realm.onCreateAccount(who, account);
|
realm.onCreateAccount(who, account);
|
||||||
return new AuthResult(newId, extId.getKey(), true);
|
return new AuthResult(newId, extId.key(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,11 +320,11 @@ public class AccountManager {
|
|||||||
private void handleSettingUserNameFailure(
|
private void handleSettingUserNameFailure(
|
||||||
ReviewDb db,
|
ReviewDb db,
|
||||||
Account account,
|
Account account,
|
||||||
AccountExternalId extId,
|
ExternalId extId,
|
||||||
String errorMessage,
|
String errorMessage,
|
||||||
Exception e,
|
Exception e,
|
||||||
boolean logException)
|
boolean logException)
|
||||||
throws AccountUserNameException, OrmException {
|
throws AccountUserNameException, OrmException, IOException, ConfigInvalidException {
|
||||||
if (logException) {
|
if (logException) {
|
||||||
log.error(errorMessage, e);
|
log.error(errorMessage, e);
|
||||||
} else {
|
} else {
|
||||||
@@ -333,16 +340,11 @@ public class AccountManager {
|
|||||||
// this is why the best we can do here is to fail early and cleanup
|
// this is why the best we can do here is to fail early and cleanup
|
||||||
// the database
|
// the database
|
||||||
db.accounts().delete(Collections.singleton(account));
|
db.accounts().delete(Collections.singleton(account));
|
||||||
db.accountExternalIds().delete(Collections.singleton(extId));
|
externalIdsUpdateFactory.create().delete(db, extId);
|
||||||
throw new AccountUserNameException(errorMessage, e);
|
throw new AccountUserNameException(errorMessage, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AccountExternalId createId(Account.Id newId, AuthRequest who) {
|
|
||||||
String ext = who.getExternalId();
|
|
||||||
return new AccountExternalId(newId, new AccountExternalId.Key(ext));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link another authentication identity to an existing account.
|
* Link another authentication identity to an existing account.
|
||||||
*
|
*
|
||||||
@@ -353,19 +355,19 @@ public class AccountManager {
|
|||||||
* this time.
|
* this time.
|
||||||
*/
|
*/
|
||||||
public AuthResult link(Account.Id to, AuthRequest who)
|
public AuthResult link(Account.Id to, AuthRequest who)
|
||||||
throws AccountException, OrmException, IOException {
|
throws AccountException, OrmException, IOException, ConfigInvalidException {
|
||||||
try (ReviewDb db = schema.open()) {
|
try (ReviewDb db = schema.open()) {
|
||||||
AccountExternalId.Key key = id(who);
|
ExternalId extId = findExternalId(who.getExternalIdKey());
|
||||||
AccountExternalId extId = getAccountExternalId(key);
|
|
||||||
if (extId != null) {
|
if (extId != null) {
|
||||||
if (!extId.getAccountId().equals(to)) {
|
if (!extId.accountId().equals(to)) {
|
||||||
throw new AccountException("Identity in use by another account");
|
throw new AccountException("Identity in use by another account");
|
||||||
}
|
}
|
||||||
update(db, who, extId);
|
update(db, who, extId);
|
||||||
} else {
|
} else {
|
||||||
extId = createId(to, who);
|
externalIdsUpdateFactory
|
||||||
extId.setEmailAddress(who.getEmailAddress());
|
.create()
|
||||||
db.accountExternalIds().insert(Collections.singleton(extId));
|
.insert(
|
||||||
|
db, ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
|
||||||
|
|
||||||
if (who.getEmailAddress() != null) {
|
if (who.getEmailAddress() != null) {
|
||||||
Account a = db.accounts().get(to);
|
Account a = db.accounts().get(to);
|
||||||
@@ -381,7 +383,7 @@ public class AccountManager {
|
|||||||
byIdCache.evict(to);
|
byIdCache.evict(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthResult(to, key, false);
|
return new AuthResult(to, who.getExternalIdKey(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,31 +401,28 @@ public class AccountManager {
|
|||||||
* this time.
|
* this time.
|
||||||
*/
|
*/
|
||||||
public AuthResult updateLink(Account.Id to, AuthRequest who)
|
public AuthResult updateLink(Account.Id to, AuthRequest who)
|
||||||
throws OrmException, AccountException, IOException {
|
throws OrmException, AccountException, IOException, ConfigInvalidException {
|
||||||
try (ReviewDb db = schema.open()) {
|
try (ReviewDb db = schema.open()) {
|
||||||
AccountExternalId.Key key = id(who);
|
Collection<ExternalId> filteredExtIdsByScheme =
|
||||||
List<AccountExternalId.Key> filteredKeysByScheme =
|
ExternalId.from(db.accountExternalIds().byAccount(to).toList())
|
||||||
filterKeysByScheme(key.getScheme(), db.accountExternalIds().byAccount(to));
|
.stream()
|
||||||
if (!filteredKeysByScheme.isEmpty()
|
.filter(e -> e.isScheme(who.getExternalIdKey().scheme()))
|
||||||
&& (filteredKeysByScheme.size() > 1 || !filteredKeysByScheme.contains(key))) {
|
.collect(toSet());
|
||||||
db.accountExternalIds().deleteKeys(filteredKeysByScheme);
|
|
||||||
|
if (!filteredExtIdsByScheme.isEmpty()
|
||||||
|
&& (filteredExtIdsByScheme.size() > 1
|
||||||
|
|| !filteredExtIdsByScheme
|
||||||
|
.stream()
|
||||||
|
.filter(e -> e.key().equals(who.getExternalIdKey()))
|
||||||
|
.findAny()
|
||||||
|
.isPresent())) {
|
||||||
|
externalIdsUpdateFactory.create().delete(db, filteredExtIdsByScheme);
|
||||||
}
|
}
|
||||||
byIdCache.evict(to);
|
byIdCache.evict(to);
|
||||||
return link(to, who);
|
return link(to, who);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AccountExternalId.Key> filterKeysByScheme(
|
|
||||||
String keyScheme, ResultSet<AccountExternalId> externalIds) {
|
|
||||||
List<AccountExternalId.Key> filteredExternalIds = new ArrayList<>();
|
|
||||||
for (AccountExternalId accountExternalId : externalIds) {
|
|
||||||
if (accountExternalId.isScheme(keyScheme)) {
|
|
||||||
filteredExternalIds.add(accountExternalId.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredExternalIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unlink an authentication identity from an existing account.
|
* Unlink an authentication identity from an existing account.
|
||||||
*
|
*
|
||||||
@@ -434,15 +433,15 @@ public class AccountManager {
|
|||||||
* at this time.
|
* at this time.
|
||||||
*/
|
*/
|
||||||
public AuthResult unlink(Account.Id from, AuthRequest who)
|
public AuthResult unlink(Account.Id from, AuthRequest who)
|
||||||
throws AccountException, OrmException, IOException {
|
throws AccountException, OrmException, IOException, ConfigInvalidException {
|
||||||
try (ReviewDb db = schema.open()) {
|
try (ReviewDb db = schema.open()) {
|
||||||
AccountExternalId.Key key = id(who);
|
ExternalId extId = findExternalId(who.getExternalIdKey());
|
||||||
AccountExternalId extId = getAccountExternalId(key);
|
|
||||||
if (extId != null) {
|
if (extId != null) {
|
||||||
if (!extId.getAccountId().equals(from)) {
|
if (!extId.accountId().equals(from)) {
|
||||||
throw new AccountException("Identity '" + key.get() + "' in use by another account");
|
throw new AccountException(
|
||||||
|
"Identity '" + who.getExternalIdKey().get() + "' in use by another account");
|
||||||
}
|
}
|
||||||
db.accountExternalIds().delete(Collections.singleton(extId));
|
externalIdsUpdateFactory.create().delete(db, extId);
|
||||||
|
|
||||||
if (who.getEmailAddress() != null) {
|
if (who.getEmailAddress() != null) {
|
||||||
Account a = db.accounts().get(from);
|
Account a = db.accounts().get(from);
|
||||||
@@ -456,14 +455,10 @@ public class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new AccountException("Identity '" + key.get() + "' not found");
|
throw new AccountException("Identity '" + who.getExternalIdKey().get() + "' not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthResult(from, key, false);
|
return new AuthResult(from, who.getExternalIdKey(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AccountExternalId.Key id(AuthRequest who) {
|
|
||||||
return new AccountExternalId.Key(who.getExternalId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -14,15 +14,15 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.server.CurrentUser.PropertyKey;
|
import com.google.gerrit.server.CurrentUser.PropertyKey;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
@@ -32,21 +32,26 @@ import java.util.Collection;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.apache.commons.codec.DecoderException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class AccountState {
|
public class AccountState {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AccountState.class);
|
||||||
|
|
||||||
public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
|
public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
|
||||||
a -> a.getAccount().getId();
|
a -> a.getAccount().getId();
|
||||||
|
|
||||||
private final Account account;
|
private final Account account;
|
||||||
private final Set<AccountGroup.UUID> internalGroups;
|
private final Set<AccountGroup.UUID> internalGroups;
|
||||||
private final Collection<AccountExternalId> externalIds;
|
private final Collection<ExternalId> externalIds;
|
||||||
private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
|
private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
|
||||||
private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
|
private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
|
||||||
|
|
||||||
public AccountState(
|
public AccountState(
|
||||||
Account account,
|
Account account,
|
||||||
Set<AccountGroup.UUID> actualGroups,
|
Set<AccountGroup.UUID> actualGroups,
|
||||||
Collection<AccountExternalId> externalIds,
|
Collection<ExternalId> externalIds,
|
||||||
Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
|
Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.internalGroups = actualGroups;
|
this.internalGroups = actualGroups;
|
||||||
@@ -63,25 +68,38 @@ public class AccountState {
|
|||||||
/**
|
/**
|
||||||
* Get the username, if one has been declared for this user.
|
* Get the username, if one has been declared for this user.
|
||||||
*
|
*
|
||||||
* <p>The username is the {@link AccountExternalId} using the scheme {@link
|
* <p>The username is the {@link ExternalId} using the scheme {@link ExternalId#SCHEME_USERNAME}.
|
||||||
* AccountExternalId#SCHEME_USERNAME}.
|
|
||||||
*/
|
*/
|
||||||
public String getUserName() {
|
public String getUserName() {
|
||||||
return account.getUserName();
|
return account.getUserName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return the password matching the requested username; or null. */
|
public boolean checkPassword(String password, String username) {
|
||||||
public String getPassword(String username) {
|
if (password == null) {
|
||||||
for (AccountExternalId id : getExternalIds()) {
|
return false;
|
||||||
if (id.isScheme(AccountExternalId.SCHEME_USERNAME) && username.equals(id.getSchemeRest())) {
|
}
|
||||||
return id.getPassword();
|
for (ExternalId id : getExternalIds()) {
|
||||||
|
// Only process the "username:$USER" entry, which is unique.
|
||||||
|
if (!id.isScheme(SCHEME_USERNAME) || !username.equals(id.key().id())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String hashedStr = id.password();
|
||||||
|
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. */
|
/** The external identities that identify the account holder. */
|
||||||
public Collection<AccountExternalId> getExternalIds() {
|
public Collection<ExternalId> getExternalIds() {
|
||||||
return externalIds;
|
return externalIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,20 +113,20 @@ public class AccountState {
|
|||||||
return internalGroups;
|
return internalGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUserName(Collection<AccountExternalId> ids) {
|
public static String getUserName(Collection<ExternalId> ids) {
|
||||||
for (AccountExternalId id : ids) {
|
for (ExternalId extId : ids) {
|
||||||
if (id.isScheme(SCHEME_USERNAME)) {
|
if (extId.isScheme(SCHEME_USERNAME)) {
|
||||||
return id.getSchemeRest();
|
return extId.key().id();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Set<String> getEmails(Collection<AccountExternalId> ids) {
|
public static Set<String> getEmails(Collection<ExternalId> ids) {
|
||||||
Set<String> emails = new HashSet<>();
|
Set<String> emails = new HashSet<>();
|
||||||
for (AccountExternalId id : ids) {
|
for (ExternalId extId : ids) {
|
||||||
if (id.isScheme(SCHEME_MAILTO)) {
|
if (extId.isScheme(SCHEME_MAILTO)) {
|
||||||
emails.add(id.getSchemeRest());
|
emails.add(extId.key().id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return emails;
|
return emails;
|
||||||
|
@@ -14,11 +14,9 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL;
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
|
||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information for {@link AccountManager#authenticate(AuthRequest)}.
|
* Information for {@link AccountManager#authenticate(AuthRequest)}.
|
||||||
@@ -30,17 +28,15 @@ import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|||||||
*/
|
*/
|
||||||
public class AuthRequest {
|
public class AuthRequest {
|
||||||
/** Create a request for a local username, such as from LDAP. */
|
/** Create a request for a local username, such as from LDAP. */
|
||||||
public static AuthRequest forUser(final String username) {
|
public static AuthRequest forUser(String username) {
|
||||||
final AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_GERRIT, username);
|
AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_GERRIT, username));
|
||||||
final AuthRequest r = new AuthRequest(i.get());
|
|
||||||
r.setUserName(username);
|
r.setUserName(username);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a request for an external username. */
|
/** Create a request for an external username. */
|
||||||
public static AuthRequest forExternalUser(String username) {
|
public static AuthRequest forExternalUser(String username) {
|
||||||
AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_EXTERNAL, username);
|
AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, username));
|
||||||
AuthRequest r = new AuthRequest(i.get());
|
|
||||||
r.setUserName(username);
|
r.setUserName(username);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
@@ -51,14 +47,13 @@ public class AuthRequest {
|
|||||||
* <p>This type of request should be used only to attach a new email address to an existing user
|
* <p>This type of request should be used only to attach a new email address to an existing user
|
||||||
* account.
|
* account.
|
||||||
*/
|
*/
|
||||||
public static AuthRequest forEmail(final String email) {
|
public static AuthRequest forEmail(String email) {
|
||||||
final AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_MAILTO, email);
|
AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_MAILTO, email));
|
||||||
final AuthRequest r = new AuthRequest(i.get());
|
|
||||||
r.setEmailAddress(email);
|
r.setEmailAddress(email);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String externalId;
|
private ExternalId.Key externalId;
|
||||||
private String password;
|
private String password;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
private String emailAddress;
|
private String emailAddress;
|
||||||
@@ -67,29 +62,24 @@ public class AuthRequest {
|
|||||||
private String authPlugin;
|
private String authPlugin;
|
||||||
private String authProvider;
|
private String authProvider;
|
||||||
|
|
||||||
public AuthRequest(final String externalId) {
|
public AuthRequest(ExternalId.Key externalId) {
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExternalId() {
|
public ExternalId.Key getExternalIdKey() {
|
||||||
return externalId;
|
return externalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isScheme(final String scheme) {
|
|
||||||
return getExternalId().startsWith(scheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocalUser() {
|
public String getLocalUser() {
|
||||||
if (isScheme(SCHEME_GERRIT)) {
|
if (externalId.isScheme(SCHEME_GERRIT)) {
|
||||||
return getExternalId().substring(SCHEME_GERRIT.length());
|
return externalId.id();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLocalUser(final String localUser) {
|
public void setLocalUser(String localUser) {
|
||||||
if (isScheme(SCHEME_GERRIT)) {
|
if (externalId.isScheme(SCHEME_GERRIT)) {
|
||||||
final AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_GERRIT, localUser);
|
externalId = ExternalId.Key.create(SCHEME_GERRIT, localUser);
|
||||||
externalId = key.get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,16 +15,14 @@
|
|||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
|
|
||||||
/** Result from {@link AccountManager#authenticate(AuthRequest)}. */
|
/** Result from {@link AccountManager#authenticate(AuthRequest)}. */
|
||||||
public class AuthResult {
|
public class AuthResult {
|
||||||
private final Account.Id accountId;
|
private final Account.Id accountId;
|
||||||
private final AccountExternalId.Key externalId;
|
private final ExternalId.Key externalId;
|
||||||
private final boolean isNew;
|
private final boolean isNew;
|
||||||
|
|
||||||
public AuthResult(
|
public AuthResult(Account.Id accountId, ExternalId.Key externalId, boolean isNew) {
|
||||||
final Account.Id accountId, final AccountExternalId.Key externalId, final boolean isNew) {
|
|
||||||
this.accountId = accountId;
|
this.accountId = accountId;
|
||||||
this.externalId = externalId;
|
this.externalId = externalId;
|
||||||
this.isNew = isNew;
|
this.isNew = isNew;
|
||||||
@@ -36,7 +34,7 @@ public class AuthResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** External identity used to authenticate the user. */
|
/** External identity used to authenticate the user. */
|
||||||
public AccountExternalId.Key getExternalId() {
|
public ExternalId.Key getExternalId() {
|
||||||
return externalId;
|
return externalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,12 +14,12 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
import static java.util.stream.Collectors.toSet;
|
||||||
|
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.common.errors.NameAlreadyUsedException;
|
import com.google.gerrit.common.errors.NameAlreadyUsedException;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.ssh.SshKeyCache;
|
import com.google.gerrit.server.ssh.SshKeyCache;
|
||||||
@@ -29,11 +29,10 @@ import com.google.gwtorm.server.OrmException;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.assistedinject.Assisted;
|
import com.google.inject.assistedinject.Assisted;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
|
||||||
/** Operation to change the username of an account. */
|
/** Operation to change the username of an account. */
|
||||||
public class ChangeUserName implements Callable<VoidResult> {
|
public class ChangeUserName implements Callable<VoidResult> {
|
||||||
@@ -48,6 +47,7 @@ public class ChangeUserName implements Callable<VoidResult> {
|
|||||||
|
|
||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
private final SshKeyCache sshKeyCache;
|
private final SshKeyCache sshKeyCache;
|
||||||
|
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
|
||||||
|
|
||||||
private final ReviewDb db;
|
private final ReviewDb db;
|
||||||
private final IdentifiedUser user;
|
private final IdentifiedUser user;
|
||||||
@@ -55,14 +55,15 @@ public class ChangeUserName implements Callable<VoidResult> {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ChangeUserName(
|
ChangeUserName(
|
||||||
final AccountCache accountCache,
|
AccountCache accountCache,
|
||||||
final SshKeyCache sshKeyCache,
|
SshKeyCache sshKeyCache,
|
||||||
@Assisted final ReviewDb db,
|
ExternalIdsUpdate.Server externalIdsUpdateFactory,
|
||||||
@Assisted final IdentifiedUser user,
|
@Assisted ReviewDb db,
|
||||||
@Nullable @Assisted final String newUsername) {
|
@Assisted IdentifiedUser user,
|
||||||
|
@Nullable @Assisted String newUsername) {
|
||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
this.sshKeyCache = sshKeyCache;
|
this.sshKeyCache = sshKeyCache;
|
||||||
|
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.newUsername = newUsername;
|
this.newUsername = newUsername;
|
||||||
@@ -70,33 +71,38 @@ public class ChangeUserName implements Callable<VoidResult> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VoidResult call()
|
public VoidResult call()
|
||||||
throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException {
|
throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException,
|
||||||
final Collection<AccountExternalId> old = old();
|
ConfigInvalidException {
|
||||||
|
Collection<ExternalId> old =
|
||||||
|
ExternalId.from(db.accountExternalIds().byAccount(user.getAccountId()).toList())
|
||||||
|
.stream()
|
||||||
|
.filter(e -> e.isScheme(SCHEME_USERNAME))
|
||||||
|
.collect(toSet());
|
||||||
if (!old.isEmpty()) {
|
if (!old.isEmpty()) {
|
||||||
throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
|
throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
|
||||||
if (newUsername != null && !newUsername.isEmpty()) {
|
if (newUsername != null && !newUsername.isEmpty()) {
|
||||||
if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
|
if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
|
||||||
throw new InvalidUserNameException();
|
throw new InvalidUserNameException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_USERNAME, newUsername);
|
ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, newUsername);
|
||||||
try {
|
try {
|
||||||
final AccountExternalId id = new AccountExternalId(user.getAccountId(), key);
|
String password = null;
|
||||||
|
for (ExternalId i : old) {
|
||||||
for (AccountExternalId i : old) {
|
if (i.password() != null) {
|
||||||
if (i.getPassword() != null) {
|
password = i.password();
|
||||||
id.setPassword(i.getPassword());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
externalIdsUpdate.insert(db, ExternalId.create(key, user.getAccountId(), null, password));
|
||||||
db.accountExternalIds().insert(Collections.singleton(id));
|
|
||||||
} catch (OrmDuplicateKeyException dupeErr) {
|
} catch (OrmDuplicateKeyException dupeErr) {
|
||||||
// If we are using this identity, don't report the exception.
|
// If we are using this identity, don't report the exception.
|
||||||
//
|
//
|
||||||
AccountExternalId other = db.accountExternalIds().get(key);
|
ExternalId other =
|
||||||
if (other != null && other.getAccountId().equals(user.getAccountId())) {
|
ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey()));
|
||||||
|
if (other != null && other.accountId().equals(user.getAccountId())) {
|
||||||
return VoidResult.INSTANCE;
|
return VoidResult.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +114,10 @@ public class ChangeUserName implements Callable<VoidResult> {
|
|||||||
|
|
||||||
// If we have any older user names, remove them.
|
// If we have any older user names, remove them.
|
||||||
//
|
//
|
||||||
db.accountExternalIds().delete(old);
|
externalIdsUpdate.delete(db, old);
|
||||||
for (AccountExternalId i : old) {
|
for (ExternalId extId : old) {
|
||||||
sshKeyCache.evict(i.getSchemeRest());
|
sshKeyCache.evict(extId.key().id());
|
||||||
accountCache.evictByUsername(i.getSchemeRest());
|
accountCache.evictByUsername(extId.key().id());
|
||||||
}
|
}
|
||||||
|
|
||||||
accountCache.evict(user.getAccountId());
|
accountCache.evict(user.getAccountId());
|
||||||
@@ -119,14 +125,4 @@ public class ChangeUserName implements Callable<VoidResult> {
|
|||||||
sshKeyCache.evict(newUsername);
|
sshKeyCache.evict(newUsername);
|
||||||
return VoidResult.INSTANCE;
|
return VoidResult.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Collection<AccountExternalId> old() throws OrmException {
|
|
||||||
final Collection<AccountExternalId> r = new ArrayList<>(1);
|
|
||||||
for (AccountExternalId i : db.accountExternalIds().byAccount(user.getAccountId())) {
|
|
||||||
if (i.isScheme(SCHEME_USERNAME)) {
|
|
||||||
r.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
|
||||||
|
|
||||||
import com.google.gerrit.audit.AuditService;
|
import com.google.gerrit.audit.AuditService;
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
import com.google.gerrit.common.data.GlobalCapability;
|
import com.google.gerrit.common.data.GlobalCapability;
|
||||||
@@ -30,7 +32,6 @@ import com.google.gerrit.extensions.restapi.RestModifyView;
|
|||||||
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
import com.google.gerrit.reviewdb.client.AccountGroupMember;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
@@ -70,6 +71,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
|
|||||||
private final AccountLoader.Factory infoLoader;
|
private final AccountLoader.Factory infoLoader;
|
||||||
private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
|
private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
|
||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
|
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
|
||||||
private final String username;
|
private final String username;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -85,6 +87,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
|
|||||||
AccountLoader.Factory infoLoader,
|
AccountLoader.Factory infoLoader,
|
||||||
DynamicSet<AccountExternalIdCreator> externalIdCreators,
|
DynamicSet<AccountExternalIdCreator> externalIdCreators,
|
||||||
AuditService auditService,
|
AuditService auditService,
|
||||||
|
ExternalIdsUpdate.User externalIdsUpdateFactory,
|
||||||
@Assisted String username) {
|
@Assisted String username) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.currentUser = currentUser;
|
this.currentUser = currentUser;
|
||||||
@@ -97,6 +100,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
|
|||||||
this.infoLoader = infoLoader;
|
this.infoLoader = infoLoader;
|
||||||
this.externalIdCreators = externalIdCreators;
|
this.externalIdCreators = externalIdCreators;
|
||||||
this.auditService = auditService;
|
this.auditService = auditService;
|
||||||
|
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,19 +124,14 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
|
|||||||
|
|
||||||
Account.Id id = new Account.Id(db.nextAccountId());
|
Account.Id id = new Account.Id(db.nextAccountId());
|
||||||
|
|
||||||
AccountExternalId extUser =
|
ExternalId extUser = ExternalId.createUsername(username, id, input.httpPassword);
|
||||||
new AccountExternalId(
|
if (db.accountExternalIds().get(extUser.key().asAccountExternalIdKey()) != null) {
|
||||||
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
|
|
||||||
|
|
||||||
if (input.httpPassword != null) {
|
|
||||||
extUser.setPassword(input.httpPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db.accountExternalIds().get(extUser.getKey()) != null) {
|
|
||||||
throw new ResourceConflictException("username '" + username + "' already exists");
|
throw new ResourceConflictException("username '" + username + "' already exists");
|
||||||
}
|
}
|
||||||
if (input.email != null) {
|
if (input.email != null) {
|
||||||
if (db.accountExternalIds().get(getEmailKey(input.email)) != null) {
|
if (db.accountExternalIds()
|
||||||
|
.get(ExternalId.Key.create(SCHEME_MAILTO, input.email).asAccountExternalIdKey())
|
||||||
|
!= null) {
|
||||||
throw new UnprocessableEntityException("email '" + input.email + "' already exists");
|
throw new UnprocessableEntityException("email '" + input.email + "' already exists");
|
||||||
}
|
}
|
||||||
if (!OutgoingEmailValidator.isValid(input.email)) {
|
if (!OutgoingEmailValidator.isValid(input.email)) {
|
||||||
@@ -140,27 +139,26 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AccountExternalId> externalIds = new ArrayList<>();
|
List<ExternalId> extIds = new ArrayList<>();
|
||||||
externalIds.add(extUser);
|
extIds.add(extUser);
|
||||||
for (AccountExternalIdCreator c : externalIdCreators) {
|
for (AccountExternalIdCreator c : externalIdCreators) {
|
||||||
externalIds.addAll(c.create(id, username, input.email));
|
extIds.addAll(c.create(id, username, input.email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
|
||||||
try {
|
try {
|
||||||
db.accountExternalIds().insert(externalIds);
|
externalIdsUpdate.insert(db, extIds);
|
||||||
} catch (OrmDuplicateKeyException duplicateKey) {
|
} catch (OrmDuplicateKeyException duplicateKey) {
|
||||||
throw new ResourceConflictException("username '" + username + "' already exists");
|
throw new ResourceConflictException("username '" + username + "' already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.email != null) {
|
if (input.email != null) {
|
||||||
AccountExternalId extMailto = new AccountExternalId(id, getEmailKey(input.email));
|
|
||||||
extMailto.setEmailAddress(input.email);
|
|
||||||
try {
|
try {
|
||||||
db.accountExternalIds().insert(Collections.singleton(extMailto));
|
externalIdsUpdate.insert(db, ExternalId.createEmail(id, input.email));
|
||||||
} catch (OrmDuplicateKeyException duplicateKey) {
|
} catch (OrmDuplicateKeyException duplicateKey) {
|
||||||
try {
|
try {
|
||||||
db.accountExternalIds().delete(Collections.singleton(extUser));
|
externalIdsUpdate.delete(db, extUser);
|
||||||
} catch (OrmException cleanupError) {
|
} catch (IOException | ConfigInvalidException | OrmException cleanupError) {
|
||||||
// Ignored
|
// Ignored
|
||||||
}
|
}
|
||||||
throw new UnprocessableEntityException("email '" + input.email + "' already exists");
|
throw new UnprocessableEntityException("email '" + input.email + "' already exists");
|
||||||
@@ -208,8 +206,4 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
|
|||||||
}
|
}
|
||||||
return groupIds;
|
return groupIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccountExternalId.Key getEmailKey(String email) {
|
|
||||||
return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -37,6 +37,7 @@ import com.google.inject.Inject;
|
|||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.assistedinject.Assisted;
|
import com.google.inject.assistedinject.Assisted;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ public class CreateEmail implements RestModifyView<AccountResource, EmailInput>
|
|||||||
public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input)
|
public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input)
|
||||||
throws AuthException, BadRequestException, ResourceConflictException,
|
throws AuthException, BadRequestException, ResourceConflictException,
|
||||||
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
|
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
|
||||||
IOException {
|
IOException, ConfigInvalidException {
|
||||||
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
|
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
|
||||||
throw new AuthException("not allowed to add email address");
|
throw new AuthException("not allowed to add email address");
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ public class CreateEmail implements RestModifyView<AccountResource, EmailInput>
|
|||||||
public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input)
|
public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input)
|
||||||
throws AuthException, BadRequestException, ResourceConflictException,
|
throws AuthException, BadRequestException, ResourceConflictException,
|
||||||
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
|
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
|
||||||
IOException {
|
IOException, ConfigInvalidException {
|
||||||
if (input.email != null && !email.equals(input.email)) {
|
if (input.email != null && !email.equals(input.email)) {
|
||||||
throw new BadRequestException("email address must match URL");
|
throw new BadRequestException("email address must match URL");
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,6 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
|||||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
import com.google.gerrit.extensions.restapi.Response;
|
import com.google.gerrit.extensions.restapi.Response;
|
||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
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.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
@@ -34,6 +33,7 @@ import com.google.inject.Provider;
|
|||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
|
public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
|
||||||
@@ -59,7 +59,7 @@ public class DeleteEmail implements RestModifyView<AccountResource.Email, Input>
|
|||||||
@Override
|
@Override
|
||||||
public Response<?> apply(AccountResource.Email rsrc, Input input)
|
public Response<?> apply(AccountResource.Email rsrc, Input input)
|
||||||
throws AuthException, ResourceNotFoundException, ResourceConflictException,
|
throws AuthException, ResourceNotFoundException, ResourceConflictException,
|
||||||
MethodNotAllowedException, OrmException, IOException {
|
MethodNotAllowedException, OrmException, IOException, ConfigInvalidException {
|
||||||
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
|
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
|
||||||
throw new AuthException("not allowed to delete email address");
|
throw new AuthException("not allowed to delete email address");
|
||||||
}
|
}
|
||||||
@@ -68,27 +68,28 @@ public class DeleteEmail implements RestModifyView<AccountResource.Email, Input>
|
|||||||
|
|
||||||
public Response<?> apply(IdentifiedUser user, String email)
|
public Response<?> apply(IdentifiedUser user, String email)
|
||||||
throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException,
|
throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException,
|
||||||
OrmException, IOException {
|
OrmException, IOException, ConfigInvalidException {
|
||||||
if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
|
if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
|
||||||
throw new MethodNotAllowedException("realm does not allow deleting emails");
|
throw new MethodNotAllowedException("realm does not allow deleting emails");
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<AccountExternalId> extIds =
|
Set<ExternalId> extIds =
|
||||||
dbProvider
|
dbProvider
|
||||||
.get()
|
.get()
|
||||||
.accountExternalIds()
|
.accountExternalIds()
|
||||||
.byAccount(user.getAccountId())
|
.byAccount(user.getAccountId())
|
||||||
.toList()
|
.toList()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(e -> email.equals(e.getEmailAddress()))
|
.map(ExternalId::from)
|
||||||
|
.filter(e -> email.equals(e.email()))
|
||||||
.collect(toSet());
|
.collect(toSet());
|
||||||
if (extIds.isEmpty()) {
|
if (extIds.isEmpty()) {
|
||||||
throw new ResourceNotFoundException(email);
|
throw new ResourceNotFoundException(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (AccountExternalId extId : extIds) {
|
for (ExternalId extId : extIds) {
|
||||||
AuthRequest authRequest = new AuthRequest(extId.getKey().get());
|
AuthRequest authRequest = new AuthRequest(extId.key());
|
||||||
authRequest.setEmailAddress(email);
|
authRequest.setEmailAddress(email);
|
||||||
accountManager.unlink(user.getAccountId(), authRequest);
|
accountManager.unlink(user.getAccountId(), authRequest);
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
|
||||||
import com.google.gerrit.extensions.restapi.AuthException;
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
@@ -24,44 +24,42 @@ import com.google.gerrit.extensions.restapi.RestApiException;
|
|||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> {
|
public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> {
|
||||||
private final Provider<ReviewDb> db;
|
|
||||||
private final AccountByEmailCache accountByEmailCache;
|
private final AccountByEmailCache accountByEmailCache;
|
||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
|
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
|
||||||
private final Provider<CurrentUser> self;
|
private final Provider<CurrentUser> self;
|
||||||
private final Provider<ReviewDb> dbProvider;
|
private final Provider<ReviewDb> dbProvider;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
DeleteExternalIds(
|
DeleteExternalIds(
|
||||||
Provider<ReviewDb> db,
|
|
||||||
AccountByEmailCache accountByEmailCache,
|
AccountByEmailCache accountByEmailCache,
|
||||||
AccountCache accountCache,
|
AccountCache accountCache,
|
||||||
|
ExternalIdsUpdate.User externalIdsUpdateFactory,
|
||||||
Provider<CurrentUser> self,
|
Provider<CurrentUser> self,
|
||||||
Provider<ReviewDb> dbProvider) {
|
Provider<ReviewDb> dbProvider) {
|
||||||
this.db = db;
|
|
||||||
this.accountByEmailCache = accountByEmailCache;
|
this.accountByEmailCache = accountByEmailCache;
|
||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
|
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
|
||||||
this.self = self;
|
this.self = self;
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<?> apply(AccountResource resource, List<String> externalIds)
|
public Response<?> apply(AccountResource resource, List<String> externalIds)
|
||||||
throws RestApiException, IOException, OrmException {
|
throws RestApiException, IOException, OrmException, ConfigInvalidException {
|
||||||
if (self.get() != resource.getUser()) {
|
if (self.get() != resource.getUser()) {
|
||||||
throw new AuthException("not allowed to delete external IDs");
|
throw new AuthException("not allowed to delete external IDs");
|
||||||
}
|
}
|
||||||
@@ -71,18 +69,20 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
|
|||||||
}
|
}
|
||||||
|
|
||||||
Account.Id accountId = resource.getUser().getAccountId();
|
Account.Id accountId = resource.getUser().getAccountId();
|
||||||
Map<AccountExternalId.Key, AccountExternalId> externalIdMap =
|
Map<ExternalId.Key, ExternalId> externalIdMap =
|
||||||
db.get()
|
dbProvider
|
||||||
|
.get()
|
||||||
.accountExternalIds()
|
.accountExternalIds()
|
||||||
.byAccount(resource.getUser().getAccountId())
|
.byAccount(resource.getUser().getAccountId())
|
||||||
.toList()
|
.toList()
|
||||||
.stream()
|
.stream()
|
||||||
.collect(Collectors.toMap(i -> i.getKey(), i -> i));
|
.map(ExternalId::from)
|
||||||
|
.collect(Collectors.toMap(i -> i.key(), i -> i));
|
||||||
|
|
||||||
List<AccountExternalId> toDelete = new ArrayList<>();
|
List<ExternalId> toDelete = new ArrayList<>();
|
||||||
AccountExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
|
ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
|
||||||
for (String externalIdStr : externalIds) {
|
for (String externalIdStr : externalIds) {
|
||||||
AccountExternalId id = externalIdMap.get(new AccountExternalId.Key(externalIdStr));
|
ExternalId id = externalIdMap.get(ExternalId.Key.parse(externalIdStr));
|
||||||
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
throw new UnprocessableEntityException(
|
throw new UnprocessableEntityException(
|
||||||
@@ -90,7 +90,7 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((!id.isScheme(SCHEME_USERNAME))
|
if ((!id.isScheme(SCHEME_USERNAME))
|
||||||
&& ((last == null) || (!last.get().equals(id.getExternalId())))) {
|
&& ((last == null) || (!last.get().equals(id.key().get())))) {
|
||||||
toDelete.add(id);
|
toDelete.add(id);
|
||||||
} else {
|
} else {
|
||||||
throw new ResourceConflictException(
|
throw new ResourceConflictException(
|
||||||
@@ -99,10 +99,10 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!toDelete.isEmpty()) {
|
if (!toDelete.isEmpty()) {
|
||||||
dbProvider.get().accountExternalIds().delete(toDelete);
|
externalIdsUpdateFactory.create().delete(dbProvider.get(), toDelete);
|
||||||
accountCache.evict(accountId);
|
accountCache.evict(accountId);
|
||||||
for (AccountExternalId e : toDelete) {
|
for (ExternalId e : toDelete) {
|
||||||
accountByEmailCache.evict(e.getEmailAddress());
|
accountByEmailCache.evict(e.email());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,321 @@
|
|||||||
|
// Copyright (C) 2016 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 java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static java.util.stream.Collectors.toSet;
|
||||||
|
|
||||||
|
import com.google.auto.value.AutoValue;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import com.google.gerrit.common.Nullable;
|
||||||
|
import com.google.gerrit.extensions.client.AuthType;
|
||||||
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
|
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
|
||||||
|
@AutoValue
|
||||||
|
public abstract class ExternalId {
|
||||||
|
private static final String EXTERNAL_ID_SECTION = "externalId";
|
||||||
|
private static final String ACCOUNT_ID_KEY = "accountId";
|
||||||
|
private static final String EMAIL_KEY = "email";
|
||||||
|
private static final String PASSWORD_KEY = "password";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheme used for {@link AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link
|
||||||
|
* AuthType#HTTP_LDAP}, and {@link AuthType#LDAP_BIND} usernames.
|
||||||
|
*
|
||||||
|
* <p>The name {@code gerrit:} was a very poor choice.
|
||||||
|
*/
|
||||||
|
public static final String SCHEME_GERRIT = "gerrit";
|
||||||
|
|
||||||
|
/** Scheme used for randomly created identities constructed by a UUID. */
|
||||||
|
public static final String SCHEME_UUID = "uuid";
|
||||||
|
|
||||||
|
/** Scheme used to represent only an email address. */
|
||||||
|
public static final String SCHEME_MAILTO = "mailto";
|
||||||
|
|
||||||
|
/** Scheme for the username used to authenticate an account, e.g. over SSH. */
|
||||||
|
public static final String SCHEME_USERNAME = "username";
|
||||||
|
|
||||||
|
/** Scheme used for GPG public keys. */
|
||||||
|
public static final String SCHEME_GPGKEY = "gpgkey";
|
||||||
|
|
||||||
|
/** Scheme for external auth used during authentication, e.g. OAuth Token */
|
||||||
|
public static final String SCHEME_EXTERNAL = "external";
|
||||||
|
|
||||||
|
@AutoValue
|
||||||
|
public abstract static class Key {
|
||||||
|
public static Key create(@Nullable String scheme, String id) {
|
||||||
|
return new AutoValue_ExternalId_Key(scheme, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId.Key from(AccountExternalId.Key externalIdKey) {
|
||||||
|
return parse(externalIdKey.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an external ID key from a string in the format "scheme:id" or "id".
|
||||||
|
*
|
||||||
|
* @return the parsed external ID key
|
||||||
|
*/
|
||||||
|
public static Key parse(String externalId) {
|
||||||
|
int c = externalId.indexOf(':');
|
||||||
|
if (c < 1 || c >= externalId.length() - 1) {
|
||||||
|
return create(null, externalId);
|
||||||
|
}
|
||||||
|
return create(externalId.substring(0, c), externalId.substring(c + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<AccountExternalId.Key> toAccountExternalIdKeys(
|
||||||
|
Collection<ExternalId.Key> extIdKeys) {
|
||||||
|
return extIdKeys.stream().map(k -> k.asAccountExternalIdKey()).collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract @Nullable String scheme();
|
||||||
|
|
||||||
|
public abstract String id();
|
||||||
|
|
||||||
|
public boolean isScheme(String scheme) {
|
||||||
|
return scheme.equals(scheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountExternalId.Key asAccountExternalIdKey() {
|
||||||
|
if (scheme() != null) {
|
||||||
|
return new AccountExternalId.Key(scheme(), id());
|
||||||
|
}
|
||||||
|
return new AccountExternalId.Key(id());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids
|
||||||
|
* notes branch.
|
||||||
|
*/
|
||||||
|
public ObjectId sha1() {
|
||||||
|
return ObjectId.fromRaw(Hashing.sha1().hashString(get(), UTF_8).asBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports this external ID key as string with the format "scheme:id", or "id" id scheme is
|
||||||
|
* null.
|
||||||
|
*
|
||||||
|
* <p>This string representation is used as subsection name in the Git config file that stores
|
||||||
|
* the external ID.
|
||||||
|
*/
|
||||||
|
public String get() {
|
||||||
|
if (scheme() != null) {
|
||||||
|
return scheme() + ":" + id();
|
||||||
|
}
|
||||||
|
return id();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId create(String scheme, String id, Account.Id accountId) {
|
||||||
|
return new AutoValue_ExternalId(Key.create(scheme, id), accountId, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId create(
|
||||||
|
String scheme,
|
||||||
|
String id,
|
||||||
|
Account.Id accountId,
|
||||||
|
@Nullable String email,
|
||||||
|
@Nullable String hashedPassword) {
|
||||||
|
return create(Key.create(scheme, id), accountId, email, hashedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId create(Key key, Account.Id accountId) {
|
||||||
|
return create(key, accountId, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId create(
|
||||||
|
Key key, Account.Id accountId, @Nullable String email, @Nullable String hashedPassword) {
|
||||||
|
return new AutoValue_ExternalId(key, accountId, email, hashedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId createWithPassword(
|
||||||
|
Key key, Account.Id accountId, @Nullable String email, String plainPassword) {
|
||||||
|
plainPassword = Strings.emptyToNull(plainPassword);
|
||||||
|
String hashedPassword =
|
||||||
|
plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
|
||||||
|
return create(key, accountId, email, hashedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId createUsername(String id, Account.Id accountId, String plainPassword) {
|
||||||
|
return createWithPassword(Key.create(SCHEME_USERNAME, id), accountId, null, plainPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId createWithEmail(
|
||||||
|
String scheme, String id, Account.Id accountId, String email) {
|
||||||
|
return createWithEmail(Key.create(scheme, id), accountId, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId createWithEmail(Key key, Account.Id accountId, String email) {
|
||||||
|
return new AutoValue_ExternalId(key, accountId, email, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId createEmail(Account.Id accountId, String email) {
|
||||||
|
return createWithEmail(SCHEME_MAILTO, email, accountId, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an external ID from a byte array that contain the external ID as an Git config file
|
||||||
|
* text.
|
||||||
|
*
|
||||||
|
* <p>The Git config must have exactly one externalId subsection with an accountId and optionally
|
||||||
|
* email and password:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* [externalId "username:jdoe"]
|
||||||
|
* accountId = 1003407
|
||||||
|
* email = jdoe@example.com
|
||||||
|
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public static ExternalId parse(String noteId, byte[] raw) throws ConfigInvalidException {
|
||||||
|
Config externalIdConfig = new Config();
|
||||||
|
try {
|
||||||
|
externalIdConfig.fromText(new String(raw, UTF_8));
|
||||||
|
} catch (ConfigInvalidException e) {
|
||||||
|
throw invalidConfig(noteId, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> externalIdKeys = externalIdConfig.getSubsections(EXTERNAL_ID_SECTION);
|
||||||
|
if (externalIdKeys.size() != 1) {
|
||||||
|
throw invalidConfig(
|
||||||
|
noteId,
|
||||||
|
String.format(
|
||||||
|
"Expected exactly 1 %s section, found %d",
|
||||||
|
EXTERNAL_ID_SECTION, externalIdKeys.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
|
||||||
|
Key externalIdKey = Key.parse(externalIdKeyStr);
|
||||||
|
if (externalIdKey == null) {
|
||||||
|
throw invalidConfig(noteId, String.format("Invalid external id: %s", externalIdKeyStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
String accountIdStr =
|
||||||
|
externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY);
|
||||||
|
String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY);
|
||||||
|
String password =
|
||||||
|
externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, PASSWORD_KEY);
|
||||||
|
if (accountIdStr == null) {
|
||||||
|
throw invalidConfig(
|
||||||
|
noteId,
|
||||||
|
String.format(
|
||||||
|
"Missing value for %s.%s.%s", EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
|
||||||
|
}
|
||||||
|
Integer accountId = Ints.tryParse(accountIdStr);
|
||||||
|
if (accountId == null) {
|
||||||
|
throw invalidConfig(
|
||||||
|
noteId,
|
||||||
|
String.format(
|
||||||
|
"Value %s for %s.%s.%s is invalid, expected account ID",
|
||||||
|
accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AutoValue_ExternalId(externalIdKey, new Account.Id(accountId), email, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConfigInvalidException invalidConfig(String noteId, String message) {
|
||||||
|
return new ConfigInvalidException(
|
||||||
|
String.format("Invalid external id config for note %s: %s", noteId, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExternalId from(AccountExternalId externalId) {
|
||||||
|
if (externalId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AutoValue_ExternalId(
|
||||||
|
ExternalId.Key.parse(externalId.getExternalId()),
|
||||||
|
externalId.getAccountId(),
|
||||||
|
externalId.getEmailAddress(),
|
||||||
|
externalId.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<ExternalId> from(Collection<AccountExternalId> externalIds) {
|
||||||
|
if (externalIds == null) {
|
||||||
|
return ImmutableSet.of();
|
||||||
|
}
|
||||||
|
return externalIds.stream().map(ExternalId::from).collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<AccountExternalId> toAccountExternalIds(Collection<ExternalId> extIds) {
|
||||||
|
return extIds.stream().map(e -> e.asAccountExternalId()).collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Key key();
|
||||||
|
|
||||||
|
public abstract Account.Id accountId();
|
||||||
|
|
||||||
|
public abstract @Nullable String email();
|
||||||
|
|
||||||
|
public abstract @Nullable String password();
|
||||||
|
|
||||||
|
public boolean isScheme(String scheme) {
|
||||||
|
return key().isScheme(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountExternalId asAccountExternalId() {
|
||||||
|
AccountExternalId extId = new AccountExternalId(accountId(), key().asAccountExternalIdKey());
|
||||||
|
extId.setEmailAddress(email());
|
||||||
|
extId.setPassword(password());
|
||||||
|
return extId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports this external ID as Git config file text.
|
||||||
|
*
|
||||||
|
* <p>The Git config has exactly one externalId subsection with an accountId and optionally email
|
||||||
|
* and password:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* [externalId "username:jdoe"]
|
||||||
|
* accountId = 1003407
|
||||||
|
* email = jdoe@example.com
|
||||||
|
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
Config c = new Config();
|
||||||
|
writeToConfig(c);
|
||||||
|
return c.toText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeToConfig(Config c) {
|
||||||
|
String externalIdKey = key().get();
|
||||||
|
c.setInt(EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, accountId().get());
|
||||||
|
if (email() != null) {
|
||||||
|
c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email());
|
||||||
|
}
|
||||||
|
if (password() != null) {
|
||||||
|
c.setString(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY, password());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (C) 2016 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 org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||||
|
|
||||||
|
import com.google.gerrit.common.Nullable;
|
||||||
|
import com.google.gerrit.reviewdb.client.RefNames;
|
||||||
|
import com.google.gerrit.server.config.AllUsersName;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.notes.NoteMap;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to read external IDs from NoteDb.
|
||||||
|
*
|
||||||
|
* <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
|
||||||
|
* refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
|
||||||
|
* is a git config file that contains an external ID. It has exactly one externalId subsection with
|
||||||
|
* an accountId and optionally email and password:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* [externalId "username:jdoe"]
|
||||||
|
* accountId = 1003407
|
||||||
|
* email = jdoe@example.com
|
||||||
|
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class ExternalIds {
|
||||||
|
public static final int MAX_NOTE_SZ = 1 << 19;
|
||||||
|
|
||||||
|
public static ObjectId readRevision(Repository repo) throws IOException {
|
||||||
|
Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
return ref != null ? ref.getObjectId() : ObjectId.zeroId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NoteMap readNoteMap(RevWalk rw, ObjectId rev) throws IOException {
|
||||||
|
if (!rev.equals(ObjectId.zeroId())) {
|
||||||
|
return NoteMap.read(rw.getObjectReader(), rw.parseCommit(rev));
|
||||||
|
}
|
||||||
|
return NoteMap.newEmptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final GitRepositoryManager repoManager;
|
||||||
|
private final AllUsersName allUsersName;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ExternalIds(GitRepositoryManager repoManager, AllUsersName allUsersName) {
|
||||||
|
this.repoManager = repoManager;
|
||||||
|
this.allUsersName = allUsersName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectId readRevision() throws IOException {
|
||||||
|
try (Repository repo = repoManager.openRepository(allUsersName)) {
|
||||||
|
return readRevision(repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads and returns the specified external ID. */
|
||||||
|
@Nullable
|
||||||
|
public ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
|
||||||
|
try (Repository repo = repoManager.openRepository(allUsersName);
|
||||||
|
RevWalk rw = new RevWalk(repo)) {
|
||||||
|
ObjectId rev = readRevision(repo);
|
||||||
|
if (rev.equals(ObjectId.zeroId())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse(key, rw, rev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalId parse(ExternalId.Key key, RevWalk rw, ObjectId rev)
|
||||||
|
throws IOException, ConfigInvalidException {
|
||||||
|
NoteMap noteMap = readNoteMap(rw, rev);
|
||||||
|
ObjectId noteId = key.sha1();
|
||||||
|
if (!noteMap.contains(noteId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] raw =
|
||||||
|
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
|
||||||
|
return ExternalId.parse(noteId.name(), raw);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright (C) 2016 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.server.account.ExternalId.toAccountExternalIds;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.GerritPersonIdent;
|
||||||
|
import com.google.gerrit.server.config.AllUsersName;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.ObjectInserter;
|
||||||
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.notes.NoteMap;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class allows to do batch updates to external IDs.
|
||||||
|
*
|
||||||
|
* <p>For NoteDb all updates will result in a single commit to the refs/meta/external-ids branch.
|
||||||
|
* This means callers can prepare many updates by invoking {@link #replace(ExternalId, ExternalId)}
|
||||||
|
* multiple times and when {@link ExternalIdsBatchUpdate#commit(ReviewDb, String)} is invoked a
|
||||||
|
* single NoteDb commit is created that contains all the prepared updates.
|
||||||
|
*/
|
||||||
|
public class ExternalIdsBatchUpdate {
|
||||||
|
private final GitRepositoryManager repoManager;
|
||||||
|
private final AllUsersName allUsersName;
|
||||||
|
private final PersonIdent serverIdent;
|
||||||
|
private final Set<ExternalId> toAdd = new HashSet<>();
|
||||||
|
private final Set<ExternalId> toDelete = new HashSet<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ExternalIdsBatchUpdate(
|
||||||
|
GitRepositoryManager repoManager,
|
||||||
|
AllUsersName allUsersName,
|
||||||
|
@GerritPersonIdent PersonIdent serverIdent) {
|
||||||
|
this.repoManager = repoManager;
|
||||||
|
this.allUsersName = allUsersName;
|
||||||
|
this.serverIdent = serverIdent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an external ID replacement to the batch.
|
||||||
|
*
|
||||||
|
* <p>The actual replacement is only done when {@link #commit(ReviewDb, String)} is invoked.
|
||||||
|
*/
|
||||||
|
public void replace(ExternalId extIdToDelete, ExternalId extIdToAdd) {
|
||||||
|
ExternalIdsUpdate.checkSameAccount(ImmutableSet.of(extIdToDelete, extIdToAdd));
|
||||||
|
toAdd.add(extIdToAdd);
|
||||||
|
toDelete.add(extIdToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits this batch.
|
||||||
|
*
|
||||||
|
* <p>This means external ID replacements which were prepared by invoking {@link
|
||||||
|
* #replace(ExternalId, ExternalId)} are now executed. Deletion of external IDs is done before
|
||||||
|
* adding the new external IDs. This means if an external ID is specified for deletion and an
|
||||||
|
* external ID with the same key is specified to be added, the old external ID with that key is
|
||||||
|
* deleted first and then the new external ID is added (so the external ID for that key is
|
||||||
|
* replaced).
|
||||||
|
*
|
||||||
|
* <p>For NoteDb a single commit is created that contains all the external ID updates.
|
||||||
|
*/
|
||||||
|
public void commit(ReviewDb db, String commitMessage)
|
||||||
|
throws IOException, OrmException, ConfigInvalidException {
|
||||||
|
if (toDelete.isEmpty() && toAdd.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.accountExternalIds().delete(toAccountExternalIds(toDelete));
|
||||||
|
db.accountExternalIds().insert(toAccountExternalIds(toAdd));
|
||||||
|
|
||||||
|
try (Repository repo = repoManager.openRepository(allUsersName);
|
||||||
|
RevWalk rw = new RevWalk(repo);
|
||||||
|
ObjectInserter ins = repo.newObjectInserter()) {
|
||||||
|
ObjectId rev = ExternalIds.readRevision(repo);
|
||||||
|
|
||||||
|
NoteMap noteMap = ExternalIds.readNoteMap(rw, rev);
|
||||||
|
|
||||||
|
for (ExternalId extId : toDelete) {
|
||||||
|
ExternalIdsUpdate.remove(rw, noteMap, extId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ExternalId extId : toAdd) {
|
||||||
|
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalIdsUpdate.commit(
|
||||||
|
repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toAdd.clear();
|
||||||
|
toDelete.clear();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,636 @@
|
|||||||
|
// Copyright (C) 2016 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.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.Key.toAccountExternalIdKeys;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
|
||||||
|
import static com.google.gerrit.server.account.ExternalIds.MAX_NOTE_SZ;
|
||||||
|
import static com.google.gerrit.server.account.ExternalIds.readNoteMap;
|
||||||
|
import static com.google.gerrit.server.account.ExternalIds.readRevision;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static java.util.stream.Collectors.toSet;
|
||||||
|
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||||
|
import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
|
||||||
|
|
||||||
|
import com.github.rholder.retry.RetryException;
|
||||||
|
import com.github.rholder.retry.Retryer;
|
||||||
|
import com.github.rholder.retry.RetryerBuilder;
|
||||||
|
import com.github.rholder.retry.StopStrategies;
|
||||||
|
import com.github.rholder.retry.WaitStrategies;
|
||||||
|
import com.google.auto.value.AutoValue;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.util.concurrent.Runnables;
|
||||||
|
import com.google.gerrit.common.Nullable;
|
||||||
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
|
import com.google.gerrit.reviewdb.client.RefNames;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.GerritPersonIdent;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.config.AllUsersName;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gerrit.server.git.LockFailureException;
|
||||||
|
import com.google.gwtorm.server.OrmDuplicateKeyException;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.lib.CommitBuilder;
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.ObjectInserter;
|
||||||
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
|
import org.eclipse.jgit.lib.RefUpdate;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.notes.NoteMap;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates externalIds in ReviewDb and NoteDb.
|
||||||
|
*
|
||||||
|
* <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
|
||||||
|
* refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
|
||||||
|
* is a git config file that contains an external ID. It has exactly one externalId subsection with
|
||||||
|
* an accountId and optionally email and password:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* [externalId "username:jdoe"]
|
||||||
|
* accountId = 1003407
|
||||||
|
* email = jdoe@example.com
|
||||||
|
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* For NoteDb each method call results in one commit on refs/meta/external-ids branch.
|
||||||
|
*/
|
||||||
|
public class ExternalIdsUpdate {
|
||||||
|
private static final String COMMIT_MSG = "Update external IDs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
|
||||||
|
*
|
||||||
|
* <p>The Gerrit server identity will be used as author and committer for all commits that update
|
||||||
|
* the external IDs.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public static class Server {
|
||||||
|
private final GitRepositoryManager repoManager;
|
||||||
|
private final AllUsersName allUsersName;
|
||||||
|
private final Provider<PersonIdent> serverIdent;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public Server(
|
||||||
|
GitRepositoryManager repoManager,
|
||||||
|
AllUsersName allUsersName,
|
||||||
|
@GerritPersonIdent Provider<PersonIdent> serverIdent) {
|
||||||
|
this.repoManager = repoManager;
|
||||||
|
this.allUsersName = allUsersName;
|
||||||
|
this.serverIdent = serverIdent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExternalIdsUpdate create() {
|
||||||
|
PersonIdent i = serverIdent.get();
|
||||||
|
return new ExternalIdsUpdate(repoManager, allUsersName, i, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the current user.
|
||||||
|
*
|
||||||
|
* <p>The identity of the current user will be used as author for all commits that update the
|
||||||
|
* external IDs. The Gerrit server identity will be used as committer.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public static class User {
|
||||||
|
private final GitRepositoryManager repoManager;
|
||||||
|
private final AllUsersName allUsersName;
|
||||||
|
private final Provider<PersonIdent> serverIdent;
|
||||||
|
private final Provider<IdentifiedUser> identifiedUser;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public User(
|
||||||
|
GitRepositoryManager repoManager,
|
||||||
|
AllUsersName allUsersName,
|
||||||
|
@GerritPersonIdent Provider<PersonIdent> serverIdent,
|
||||||
|
Provider<IdentifiedUser> identifiedUser) {
|
||||||
|
this.repoManager = repoManager;
|
||||||
|
this.allUsersName = allUsersName;
|
||||||
|
this.serverIdent = serverIdent;
|
||||||
|
this.identifiedUser = identifiedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExternalIdsUpdate create() {
|
||||||
|
PersonIdent i = serverIdent.get();
|
||||||
|
return new ExternalIdsUpdate(
|
||||||
|
repoManager, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
|
||||||
|
return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static RetryerBuilder<Void> retryerBuilder() {
|
||||||
|
return RetryerBuilder.<Void>newBuilder()
|
||||||
|
.retryIfException(e -> e instanceof LockFailureException)
|
||||||
|
.withWaitStrategy(
|
||||||
|
WaitStrategies.join(
|
||||||
|
WaitStrategies.exponentialWait(2, TimeUnit.SECONDS),
|
||||||
|
WaitStrategies.randomWait(50, TimeUnit.MILLISECONDS)))
|
||||||
|
.withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Retryer<Void> RETRYER = retryerBuilder().build();
|
||||||
|
|
||||||
|
private final GitRepositoryManager repoManager;
|
||||||
|
private final AllUsersName allUsersName;
|
||||||
|
private final PersonIdent committerIdent;
|
||||||
|
private final PersonIdent authorIdent;
|
||||||
|
private final Runnable afterReadRevision;
|
||||||
|
private final Retryer<Void> retryer;
|
||||||
|
|
||||||
|
private ExternalIdsUpdate(
|
||||||
|
GitRepositoryManager repoManager,
|
||||||
|
AllUsersName allUsersName,
|
||||||
|
PersonIdent committerIdent,
|
||||||
|
PersonIdent authorIdent) {
|
||||||
|
this(repoManager, allUsersName, committerIdent, authorIdent, Runnables.doNothing(), RETRYER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public ExternalIdsUpdate(
|
||||||
|
GitRepositoryManager repoManager,
|
||||||
|
AllUsersName allUsersName,
|
||||||
|
PersonIdent committerIdent,
|
||||||
|
PersonIdent authorIdent,
|
||||||
|
Runnable afterReadRevision,
|
||||||
|
Retryer<Void> retryer) {
|
||||||
|
this.repoManager = checkNotNull(repoManager, "repoManager");
|
||||||
|
this.allUsersName = checkNotNull(allUsersName, "allUsersName");
|
||||||
|
this.committerIdent = checkNotNull(committerIdent, "committerIdent");
|
||||||
|
this.authorIdent = checkNotNull(authorIdent, "authorIdent");
|
||||||
|
this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
|
||||||
|
this.retryer = checkNotNull(retryer, "retryer");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new external ID.
|
||||||
|
*
|
||||||
|
* <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
|
||||||
|
*/
|
||||||
|
public void insert(ReviewDb db, ExternalId extId)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
insert(db, Collections.singleton(extId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts new external IDs.
|
||||||
|
*
|
||||||
|
* <p>If any of the external ID already exists, the insert fails with {@link
|
||||||
|
* OrmDuplicateKeyException}.
|
||||||
|
*/
|
||||||
|
public void insert(ReviewDb db, Collection<ExternalId> extIds)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
db.accountExternalIds().insert(toAccountExternalIds(extIds));
|
||||||
|
|
||||||
|
updateNoteMap(
|
||||||
|
o -> {
|
||||||
|
for (ExternalId extId : extIds) {
|
||||||
|
insert(o.rw(), o.ins(), o.noteMap(), extId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts or updates an external ID.
|
||||||
|
*
|
||||||
|
* <p>If the external ID already exists, it is overwritten, otherwise it is inserted.
|
||||||
|
*/
|
||||||
|
public void upsert(ReviewDb db, ExternalId extId)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
upsert(db, Collections.singleton(extId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts or updates external IDs.
|
||||||
|
*
|
||||||
|
* <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted.
|
||||||
|
*/
|
||||||
|
public void upsert(ReviewDb db, Collection<ExternalId> extIds)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
db.accountExternalIds().upsert(toAccountExternalIds(extIds));
|
||||||
|
|
||||||
|
updateNoteMap(
|
||||||
|
o -> {
|
||||||
|
for (ExternalId extId : extIds) {
|
||||||
|
upsert(o.rw(), o.ins(), o.noteMap(), extId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an external ID.
|
||||||
|
*
|
||||||
|
* <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID
|
||||||
|
* that has the same key, but otherwise doesn't match the specified external ID.
|
||||||
|
*/
|
||||||
|
public void delete(ReviewDb db, ExternalId extId)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
delete(db, Collections.singleton(extId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes external IDs.
|
||||||
|
*
|
||||||
|
* <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID
|
||||||
|
* that has the same key as any of the external IDs that should be deleted, but otherwise doesn't
|
||||||
|
* match the that external ID.
|
||||||
|
*/
|
||||||
|
public void delete(ReviewDb db, Collection<ExternalId> extIds)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
db.accountExternalIds().delete(toAccountExternalIds(extIds));
|
||||||
|
|
||||||
|
updateNoteMap(
|
||||||
|
o -> {
|
||||||
|
for (ExternalId extId : extIds) {
|
||||||
|
remove(o.rw(), o.noteMap(), extId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an external ID by key.
|
||||||
|
*
|
||||||
|
* <p>The external ID is only deleted if it belongs to the specified account. If it belongs to
|
||||||
|
* another account the deletion fails with {@link IllegalStateException}.
|
||||||
|
*/
|
||||||
|
public void delete(ReviewDb db, Account.Id accountId, ExternalId.Key extIdKey)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
delete(db, accountId, Collections.singleton(extIdKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete external IDs by external ID key.
|
||||||
|
*
|
||||||
|
* <p>The external IDs are only deleted if they belongs to the specified account. If any of the
|
||||||
|
* external IDs belongs to another account the deletion fails with {@link IllegalStateException}.
|
||||||
|
*/
|
||||||
|
public void delete(ReviewDb db, Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(extIdKeys));
|
||||||
|
|
||||||
|
updateNoteMap(
|
||||||
|
o -> {
|
||||||
|
for (ExternalId.Key extIdKey : extIdKeys) {
|
||||||
|
remove(o.rw(), o.noteMap(), accountId, extIdKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deletes all external IDs of the specified account. */
|
||||||
|
public void deleteAll(ReviewDb db, Account.Id accountId)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
delete(db, ExternalId.from(db.accountExternalIds().byAccount(accountId).toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces external IDs for an account by external ID keys.
|
||||||
|
*
|
||||||
|
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
|
||||||
|
* external ID key is specified for deletion and an external ID with the same key is specified to
|
||||||
|
* be added, the old external ID with that key is deleted first and then the new external ID is
|
||||||
|
* added (so the external ID for that key is replaced).
|
||||||
|
*
|
||||||
|
* <p>If any of the specified external IDs belongs to another account the replacement fails with
|
||||||
|
* {@link IllegalStateException}.
|
||||||
|
*/
|
||||||
|
public void replace(
|
||||||
|
ReviewDb db,
|
||||||
|
Account.Id accountId,
|
||||||
|
Collection<ExternalId.Key> toDelete,
|
||||||
|
Collection<ExternalId> toAdd)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
checkSameAccount(toAdd, accountId);
|
||||||
|
|
||||||
|
db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(toDelete));
|
||||||
|
db.accountExternalIds().insert(toAccountExternalIds(toAdd));
|
||||||
|
|
||||||
|
updateNoteMap(
|
||||||
|
o -> {
|
||||||
|
for (ExternalId.Key extIdKey : toDelete) {
|
||||||
|
remove(o.rw(), o.noteMap(), accountId, extIdKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ExternalId extId : toAdd) {
|
||||||
|
insert(o.rw(), o.ins(), o.noteMap(), extId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces an external ID.
|
||||||
|
*
|
||||||
|
* <p>If the specified external IDs belongs to different accounts the replacement fails with
|
||||||
|
* {@link IllegalStateException}.
|
||||||
|
*/
|
||||||
|
public void replace(ReviewDb db, ExternalId toDelete, ExternalId toAdd)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
replace(db, Collections.singleton(toDelete), Collections.singleton(toAdd));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces external IDs.
|
||||||
|
*
|
||||||
|
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
|
||||||
|
* external ID is specified for deletion and an external ID with the same key is specified to be
|
||||||
|
* added, the old external ID with that key is deleted first and then the new external ID is added
|
||||||
|
* (so the external ID for that key is replaced).
|
||||||
|
*
|
||||||
|
* <p>If the specified external IDs belong to different accounts the replacement fails with {@link
|
||||||
|
* IllegalStateException}.
|
||||||
|
*/
|
||||||
|
public void replace(ReviewDb db, Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
|
||||||
|
if (accountId == null) {
|
||||||
|
// toDelete and toAdd are empty -> nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
replace(db, accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that all specified external IDs belong to the same account.
|
||||||
|
*
|
||||||
|
* @return the ID of the account to which all specified external IDs belong.
|
||||||
|
*/
|
||||||
|
public static Account.Id checkSameAccount(Iterable<ExternalId> extIds) {
|
||||||
|
return checkSameAccount(extIds, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that all specified external IDs belong to specified account. If no account is specified
|
||||||
|
* it is checked that all specified external IDs belong to the same account.
|
||||||
|
*
|
||||||
|
* @return the ID of the account to which all specified external IDs belong.
|
||||||
|
*/
|
||||||
|
public static Account.Id checkSameAccount(
|
||||||
|
Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
|
||||||
|
for (ExternalId extId : extIds) {
|
||||||
|
if (accountId == null) {
|
||||||
|
accountId = extId.accountId();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
checkState(
|
||||||
|
accountId.equals(extId.accountId()),
|
||||||
|
"external id %s belongs to account %s, expected account %s",
|
||||||
|
extId.key().get(),
|
||||||
|
extId.accountId().get(),
|
||||||
|
accountId.get());
|
||||||
|
}
|
||||||
|
return accountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new external ID and sets it in the note map.
|
||||||
|
*
|
||||||
|
* <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
|
||||||
|
*/
|
||||||
|
public static void insert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
|
||||||
|
throws OrmDuplicateKeyException, ConfigInvalidException, IOException {
|
||||||
|
if (noteMap.contains(extId.key().sha1())) {
|
||||||
|
throw new OrmDuplicateKeyException(
|
||||||
|
String.format("external id %s already exists", extId.key().get()));
|
||||||
|
}
|
||||||
|
upsert(rw, ins, noteMap, extId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert or updates an new external ID and sets it in the note map.
|
||||||
|
*
|
||||||
|
* <p>If the external ID already exists it is overwritten.
|
||||||
|
*/
|
||||||
|
private static void upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
|
||||||
|
throws IOException, ConfigInvalidException {
|
||||||
|
ObjectId noteId = extId.key().sha1();
|
||||||
|
Config c = new Config();
|
||||||
|
if (noteMap.contains(extId.key().sha1())) {
|
||||||
|
byte[] raw =
|
||||||
|
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
|
||||||
|
try {
|
||||||
|
c.fromText(new String(raw, UTF_8));
|
||||||
|
} catch (ConfigInvalidException e) {
|
||||||
|
throw new ConfigInvalidException(
|
||||||
|
String.format("Invalid external id config for note %s: %s", noteId, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extId.writeToConfig(c);
|
||||||
|
byte[] raw = c.toText().getBytes(UTF_8);
|
||||||
|
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
|
||||||
|
noteMap.set(noteId, dataBlob);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an external ID from the note map.
|
||||||
|
*
|
||||||
|
* <p>The removal fails with {@link IllegalStateException} if there is an existing external ID
|
||||||
|
* that has the same key, but otherwise doesn't match the specified external ID.
|
||||||
|
*/
|
||||||
|
public static void remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
|
||||||
|
throws IOException, ConfigInvalidException {
|
||||||
|
ObjectId noteId = extId.key().sha1();
|
||||||
|
if (!noteMap.contains(noteId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] raw =
|
||||||
|
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
|
||||||
|
ExternalId actualExtId = ExternalId.parse(noteId.name(), raw);
|
||||||
|
checkState(
|
||||||
|
extId.equals(actualExtId),
|
||||||
|
"external id %s should be removed, but it's not matching the actual external id %s",
|
||||||
|
extId.toString(),
|
||||||
|
actualExtId.toString());
|
||||||
|
noteMap.remove(noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an external ID from the note map by external ID key.
|
||||||
|
*
|
||||||
|
* <p>The external ID is only deleted if it belongs to the specified account. If the external IDs
|
||||||
|
* belongs to another account the deletion fails with {@link IllegalStateException}.
|
||||||
|
*/
|
||||||
|
private static void remove(
|
||||||
|
RevWalk rw, NoteMap noteMap, Account.Id accountId, ExternalId.Key extIdKey)
|
||||||
|
throws IOException, ConfigInvalidException {
|
||||||
|
ObjectId noteId = extIdKey.sha1();
|
||||||
|
if (!noteMap.contains(noteId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] raw =
|
||||||
|
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
|
||||||
|
ExternalId extId = ExternalId.parse(noteId.name(), raw);
|
||||||
|
checkState(
|
||||||
|
accountId.equals(extId.accountId()),
|
||||||
|
"external id %s should be removed for account %s,"
|
||||||
|
+ " but external id belongs to account %s",
|
||||||
|
extIdKey.get(),
|
||||||
|
accountId.get(),
|
||||||
|
extId.accountId().get());
|
||||||
|
noteMap.remove(noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNoteMap(MyConsumer<OpenRepo> update)
|
||||||
|
throws IOException, ConfigInvalidException, OrmException {
|
||||||
|
try (Repository repo = repoManager.openRepository(allUsersName);
|
||||||
|
RevWalk rw = new RevWalk(repo);
|
||||||
|
ObjectInserter ins = repo.newObjectInserter()) {
|
||||||
|
retryer.call(new TryNoteMapUpdate(repo, rw, ins, update));
|
||||||
|
} catch (ExecutionException | RetryException e) {
|
||||||
|
if (e.getCause() != null) {
|
||||||
|
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
|
||||||
|
Throwables.throwIfInstanceOf(e.getCause(), ConfigInvalidException.class);
|
||||||
|
Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
|
||||||
|
}
|
||||||
|
throw new OrmException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void commit(
|
||||||
|
Repository repo, RevWalk rw, ObjectInserter ins, ObjectId rev, NoteMap noteMap)
|
||||||
|
throws IOException {
|
||||||
|
commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, committerIdent, authorIdent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Commits updates to the external IDs. */
|
||||||
|
public static void commit(
|
||||||
|
Repository repo,
|
||||||
|
RevWalk rw,
|
||||||
|
ObjectInserter ins,
|
||||||
|
ObjectId rev,
|
||||||
|
NoteMap noteMap,
|
||||||
|
String commitMessage,
|
||||||
|
PersonIdent committerIdent,
|
||||||
|
PersonIdent authorIdent)
|
||||||
|
throws IOException {
|
||||||
|
CommitBuilder cb = new CommitBuilder();
|
||||||
|
cb.setMessage(commitMessage);
|
||||||
|
cb.setTreeId(noteMap.writeTree(ins));
|
||||||
|
cb.setAuthor(authorIdent);
|
||||||
|
cb.setCommitter(committerIdent);
|
||||||
|
if (!rev.equals(ObjectId.zeroId())) {
|
||||||
|
cb.setParentId(rev);
|
||||||
|
} else {
|
||||||
|
cb.setParentIds(); // Ref is currently nonexistent, commit has no parents.
|
||||||
|
}
|
||||||
|
if (cb.getTreeId() == null) {
|
||||||
|
if (rev.equals(ObjectId.zeroId())) {
|
||||||
|
cb.setTreeId(emptyTree(ins)); // No parent, assume empty tree.
|
||||||
|
} else {
|
||||||
|
RevCommit p = rw.parseCommit(rev);
|
||||||
|
cb.setTreeId(p.getTree()); // Copy tree from parent.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectId commitId = ins.insert(cb);
|
||||||
|
ins.flush();
|
||||||
|
|
||||||
|
RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
u.setRefLogIdent(committerIdent);
|
||||||
|
u.setRefLogMessage("Update external IDs", false);
|
||||||
|
u.setExpectedOldObjectId(rev);
|
||||||
|
u.setNewObjectId(commitId);
|
||||||
|
RefUpdate.Result res = u.update();
|
||||||
|
switch (res) {
|
||||||
|
case NEW:
|
||||||
|
case FAST_FORWARD:
|
||||||
|
case NO_CHANGE:
|
||||||
|
case RENAMED:
|
||||||
|
case FORCED:
|
||||||
|
break;
|
||||||
|
case LOCK_FAILURE:
|
||||||
|
throw new LockFailureException("Updating external IDs failed with " + res);
|
||||||
|
case IO_FAILURE:
|
||||||
|
case NOT_ATTEMPTED:
|
||||||
|
case REJECTED:
|
||||||
|
case REJECTED_CURRENT_BRANCH:
|
||||||
|
default:
|
||||||
|
throw new IOException("Updating external IDs failed with " + res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
|
||||||
|
return ins.insert(OBJ_TREE, new byte[] {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static interface MyConsumer<T> {
|
||||||
|
void accept(T t) throws IOException, ConfigInvalidException, OrmException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AutoValue
|
||||||
|
abstract static class OpenRepo {
|
||||||
|
static OpenRepo create(Repository repo, RevWalk rw, ObjectInserter ins, NoteMap noteMap) {
|
||||||
|
return new AutoValue_ExternalIdsUpdate_OpenRepo(repo, rw, ins, noteMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract Repository repo();
|
||||||
|
|
||||||
|
abstract RevWalk rw();
|
||||||
|
|
||||||
|
abstract ObjectInserter ins();
|
||||||
|
|
||||||
|
abstract NoteMap noteMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TryNoteMapUpdate implements Callable<Void> {
|
||||||
|
private final Repository repo;
|
||||||
|
private final RevWalk rw;
|
||||||
|
private final ObjectInserter ins;
|
||||||
|
private final MyConsumer<OpenRepo> update;
|
||||||
|
|
||||||
|
private TryNoteMapUpdate(
|
||||||
|
Repository repo, RevWalk rw, ObjectInserter ins, MyConsumer<OpenRepo> update) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.rw = rw;
|
||||||
|
this.ins = ins;
|
||||||
|
this.update = update;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void call() throws Exception {
|
||||||
|
ObjectId rev = readRevision(repo);
|
||||||
|
|
||||||
|
afterReadRevision.run();
|
||||||
|
|
||||||
|
NoteMap noteMap = readNoteMap(rw, rev);
|
||||||
|
update.accept(OpenRepo.create(repo, rw, ins, noteMap));
|
||||||
|
|
||||||
|
commit(repo, rw, ins, rev, noteMap);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
@@ -22,7 +22,6 @@ import com.google.gerrit.extensions.common.AccountExternalIdInfo;
|
|||||||
import com.google.gerrit.extensions.restapi.AuthException;
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
@@ -54,22 +53,23 @@ public class GetExternalIds implements RestReadView<AccountResource> {
|
|||||||
throw new AuthException("not allowed to get external IDs");
|
throw new AuthException("not allowed to get external IDs");
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection<AccountExternalId> ids =
|
Collection<ExternalId> ids =
|
||||||
db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList();
|
ExternalId.from(
|
||||||
|
db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList());
|
||||||
if (ids.isEmpty()) {
|
if (ids.isEmpty()) {
|
||||||
return ImmutableList.of();
|
return ImmutableList.of();
|
||||||
}
|
}
|
||||||
List<AccountExternalIdInfo> result = Lists.newArrayListWithCapacity(ids.size());
|
List<AccountExternalIdInfo> result = Lists.newArrayListWithCapacity(ids.size());
|
||||||
for (AccountExternalId id : ids) {
|
for (ExternalId id : ids) {
|
||||||
AccountExternalIdInfo info = new AccountExternalIdInfo();
|
AccountExternalIdInfo info = new AccountExternalIdInfo();
|
||||||
info.identity = id.getExternalId();
|
info.identity = id.key().get();
|
||||||
info.emailAddress = id.getEmailAddress();
|
info.emailAddress = id.email();
|
||||||
info.trusted = toBoolean(authConfig.isIdentityTrustable(Collections.singleton(id)));
|
info.trusted = toBoolean(authConfig.isIdentityTrustable(Collections.singleton(id)));
|
||||||
// The identity can be deleted only if its not the one used to
|
// The identity can be deleted only if its not the one used to
|
||||||
// establish this web session, and if only if an identity was
|
// establish this web session, and if only if an identity was
|
||||||
// actually used to establish this web session.
|
// actually used to establish this web session.
|
||||||
if (!id.isScheme(SCHEME_USERNAME)) {
|
if (!id.isScheme(SCHEME_USERNAME)) {
|
||||||
AccountExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
|
ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
|
||||||
info.canDelete = toBoolean(last == null || !last.get().equals(info.identity));
|
info.canDelete = toBoolean(last == null || !last.get().equals(info.identity));
|
||||||
}
|
}
|
||||||
result.add(info);
|
result.add(info);
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -20,7 +20,6 @@ import com.google.gerrit.extensions.common.AccountInfo;
|
|||||||
import com.google.gerrit.extensions.common.AvatarInfo;
|
import com.google.gerrit.extensions.common.AvatarInfo;
|
||||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.avatar.AvatarProvider;
|
import com.google.gerrit.server.avatar.AvatarProvider;
|
||||||
import com.google.inject.AbstractModule;
|
import com.google.inject.AbstractModule;
|
||||||
@@ -74,7 +73,7 @@ public class InternalAccountDirectory extends AccountDirectory {
|
|||||||
private void fill(
|
private void fill(
|
||||||
AccountInfo info,
|
AccountInfo info,
|
||||||
Account account,
|
Account account,
|
||||||
@Nullable Collection<AccountExternalId> externalIds,
|
@Nullable Collection<ExternalId> externalIds,
|
||||||
Set<FillOptions> options) {
|
Set<FillOptions> options) {
|
||||||
if (options.contains(FillOptions.ID)) {
|
if (options.contains(FillOptions.ID)) {
|
||||||
info._accountId = account.getId().get();
|
info._accountId = account.getId().get();
|
||||||
@@ -124,8 +123,7 @@ public class InternalAccountDirectory extends AccountDirectory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getSecondaryEmails(
|
public List<String> getSecondaryEmails(Account account, Collection<ExternalId> externalIds) {
|
||||||
Account account, Collection<AccountExternalId> externalIds) {
|
|
||||||
List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds));
|
List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds));
|
||||||
if (account.getPreferredEmail() != null) {
|
if (account.getPreferredEmail() != null) {
|
||||||
emails.remove(account.getPreferredEmail());
|
emails.remove(account.getPreferredEmail());
|
||||||
|
@@ -56,7 +56,6 @@ public class Module extends RestApiModule {
|
|||||||
put(EMAIL_KIND).to(PutEmail.class);
|
put(EMAIL_KIND).to(PutEmail.class);
|
||||||
delete(EMAIL_KIND).to(DeleteEmail.class);
|
delete(EMAIL_KIND).to(DeleteEmail.class);
|
||||||
put(EMAIL_KIND, "preferred").to(PutPreferred.class);
|
put(EMAIL_KIND, "preferred").to(PutPreferred.class);
|
||||||
get(ACCOUNT_KIND, "password.http").to(GetHttpPassword.class);
|
|
||||||
put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
|
put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
|
||||||
delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
|
delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
|
||||||
child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
|
child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.account;
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.gerrit.extensions.restapi.AuthException;
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
@@ -22,7 +22,6 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
|||||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
import com.google.gerrit.extensions.restapi.Response;
|
import com.google.gerrit.extensions.restapi.Response;
|
||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
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.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
@@ -30,14 +29,12 @@ import com.google.gerrit.server.account.PutHttpPassword.Input;
|
|||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Collections;
|
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
|
public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
|
||||||
public static class Input {
|
public static class Input {
|
||||||
public String httpPassword;
|
public String httpPassword;
|
||||||
@@ -58,19 +55,24 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
|
|||||||
private final Provider<CurrentUser> self;
|
private final Provider<CurrentUser> self;
|
||||||
private final Provider<ReviewDb> dbProvider;
|
private final Provider<ReviewDb> dbProvider;
|
||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
|
private final ExternalIdsUpdate.User externalIdsUpdate;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
PutHttpPassword(
|
PutHttpPassword(
|
||||||
Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, AccountCache accountCache) {
|
Provider<CurrentUser> self,
|
||||||
|
Provider<ReviewDb> dbProvider,
|
||||||
|
AccountCache accountCache,
|
||||||
|
ExternalIdsUpdate.User externalIdsUpdate) {
|
||||||
this.self = self;
|
this.self = self;
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
|
this.externalIdsUpdate = externalIdsUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<String> apply(AccountResource rsrc, Input input)
|
public Response<String> apply(AccountResource rsrc, Input input)
|
||||||
throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
|
throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
|
||||||
IOException {
|
IOException, ConfigInvalidException {
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
input = new Input();
|
input = new Input();
|
||||||
}
|
}
|
||||||
@@ -100,21 +102,26 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Response<String> apply(IdentifiedUser user, String newPassword)
|
public Response<String> apply(IdentifiedUser user, String newPassword)
|
||||||
throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException {
|
throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException,
|
||||||
|
ConfigInvalidException {
|
||||||
if (user.getUserName() == null) {
|
if (user.getUserName() == null) {
|
||||||
throw new ResourceConflictException("username must be set");
|
throw new ResourceConflictException("username must be set");
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountExternalId id =
|
ExternalId extId =
|
||||||
dbProvider
|
ExternalId.from(
|
||||||
.get()
|
dbProvider
|
||||||
.accountExternalIds()
|
.get()
|
||||||
.get(new AccountExternalId.Key(SCHEME_USERNAME, user.getUserName()));
|
.accountExternalIds()
|
||||||
if (id == null) {
|
.get(
|
||||||
|
ExternalId.Key.create(SCHEME_USERNAME, user.getUserName())
|
||||||
|
.asAccountExternalIdKey()));
|
||||||
|
if (extId == null) {
|
||||||
throw new ResourceNotFoundException();
|
throw new ResourceNotFoundException();
|
||||||
}
|
}
|
||||||
id.setPassword(newPassword);
|
ExternalId newExtId =
|
||||||
dbProvider.get().accountExternalIds().update(Collections.singleton(id));
|
ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword);
|
||||||
|
externalIdsUpdate.create().upsert(dbProvider.get(), newExtId);
|
||||||
accountCache.evict(user.getAccountId());
|
accountCache.evict(user.getAccountId());
|
||||||
|
|
||||||
return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);
|
return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);
|
||||||
|
@@ -30,6 +30,7 @@ import com.google.inject.Inject;
|
|||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class PutUsername implements RestModifyView<AccountResource, Input> {
|
public class PutUsername implements RestModifyView<AccountResource, Input> {
|
||||||
@@ -57,7 +58,7 @@ public class PutUsername implements RestModifyView<AccountResource, Input> {
|
|||||||
@Override
|
@Override
|
||||||
public String apply(AccountResource rsrc, Input input)
|
public String apply(AccountResource rsrc, Input input)
|
||||||
throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
|
throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
|
||||||
ResourceConflictException, OrmException, IOException {
|
ResourceConflictException, OrmException, IOException, ConfigInvalidException {
|
||||||
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
|
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
|
||||||
throw new AuthException("not allowed to set username");
|
throw new AuthException("not allowed to set username");
|
||||||
}
|
}
|
||||||
|
@@ -372,7 +372,7 @@ public class AccountApiImpl implements AccountApi {
|
|||||||
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
|
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
|
||||||
try {
|
try {
|
||||||
createEmailFactory.create(input.email).apply(rsrc, input);
|
createEmailFactory.create(input.email).apply(rsrc, input);
|
||||||
} catch (EmailException | OrmException | IOException e) {
|
} catch (EmailException | OrmException | IOException | ConfigInvalidException e) {
|
||||||
throw new RestApiException("Cannot add email", e);
|
throw new RestApiException("Cannot add email", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +382,7 @@ public class AccountApiImpl implements AccountApi {
|
|||||||
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email);
|
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email);
|
||||||
try {
|
try {
|
||||||
deleteEmail.apply(rsrc, null);
|
deleteEmail.apply(rsrc, null);
|
||||||
} catch (OrmException | IOException e) {
|
} catch (OrmException | IOException | ConfigInvalidException e) {
|
||||||
throw new RestApiException("Cannot delete email", e);
|
throw new RestApiException("Cannot delete email", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,7 +494,7 @@ public class AccountApiImpl implements AccountApi {
|
|||||||
public void deleteExternalIds(List<String> externalIds) throws RestApiException {
|
public void deleteExternalIds(List<String> externalIds) throws RestApiException {
|
||||||
try {
|
try {
|
||||||
deleteExternalIds.apply(account, externalIds);
|
deleteExternalIds.apply(account, externalIds);
|
||||||
} catch (IOException | OrmException e) {
|
} catch (IOException | OrmException | ConfigInvalidException e) {
|
||||||
throw new RestApiException("Cannot delete external IDs", e);
|
throw new RestApiException("Cannot delete external IDs", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
package com.google.gerrit.server.api.accounts;
|
package com.google.gerrit.server.api.accounts;
|
||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface AccountExternalIdCreator {
|
public interface AccountExternalIdCreator {
|
||||||
@@ -28,5 +28,5 @@ public interface AccountExternalIdCreator {
|
|||||||
* @param email an optional email address to assign to the external identifiers, or {@code null}.
|
* @param email an optional email address to assign to the external identifiers, or {@code null}.
|
||||||
* @return a list of external identifiers, or an empty list.
|
* @return a list of external identifiers, or an empty list.
|
||||||
*/
|
*/
|
||||||
List<AccountExternalId> create(Account.Id id, String username, String email);
|
List<ExternalId> create(Account.Id id, String username, String email);
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,6 @@
|
|||||||
package com.google.gerrit.server.auth;
|
package com.google.gerrit.server.auth;
|
||||||
|
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/** Defines an abstract request for user authentication to Gerrit. */
|
/** Defines an abstract request for user authentication to Gerrit. */
|
||||||
public abstract class AuthRequest {
|
public abstract class AuthRequest {
|
||||||
@@ -46,10 +45,4 @@ public abstract class AuthRequest {
|
|||||||
public final String getPassword() {
|
public final String getPassword() {
|
||||||
return password;
|
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";
|
return "gerrit";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(gerritcodereview-team): This function has no coverage.
|
||||||
@Override
|
@Override
|
||||||
public AuthUser authenticate(AuthRequest req)
|
public AuthUser authenticate(AuthRequest req)
|
||||||
throws MissingCredentialsException, InvalidCredentialsException, UnknownUserException,
|
throws MissingCredentialsException, InvalidCredentialsException, UnknownUserException,
|
||||||
@@ -63,7 +64,9 @@ public class InternalAuthBackend implements AuthBackend {
|
|||||||
+ ": account inactive or not provisioned in Gerrit");
|
+ ": 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);
|
return new AuthUser(AuthUser.UUID.create(username), username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.auth.ldap;
|
package com.google.gerrit.server.auth.ldap;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
|
||||||
import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
|
import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
|
||||||
import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
|
import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
|
||||||
import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
|
import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
|
||||||
@@ -25,10 +26,10 @@ import com.google.gerrit.common.Nullable;
|
|||||||
import com.google.gerrit.common.data.GroupDescription;
|
import com.google.gerrit.common.data.GroupDescription;
|
||||||
import com.google.gerrit.common.data.GroupReference;
|
import com.google.gerrit.common.data.GroupReference;
|
||||||
import com.google.gerrit.common.data.ParameterizedString;
|
import com.google.gerrit.common.data.ParameterizedString;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.account.GroupBackend;
|
import com.google.gerrit.server.account.GroupBackend;
|
||||||
import com.google.gerrit.server.account.GroupMembership;
|
import com.google.gerrit.server.account.GroupMembership;
|
||||||
import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
|
import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
|
||||||
@@ -180,10 +181,10 @@ public class LdapGroupBackend implements GroupBackend {
|
|||||||
return new LdapGroupMembership(membershipCache, projectCache, id);
|
return new LdapGroupMembership(membershipCache, projectCache, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String findId(final Collection<AccountExternalId> ids) {
|
private static String findId(Collection<ExternalId> extIds) {
|
||||||
for (final AccountExternalId i : ids) {
|
for (ExternalId extId : extIds) {
|
||||||
if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
|
if (extId.isScheme(SCHEME_GERRIT)) {
|
||||||
return i.getSchemeRest();
|
return extId.key().id();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.auth.ldap;
|
package com.google.gerrit.server.auth.ldap;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
@@ -24,13 +24,13 @@ import com.google.gerrit.common.data.ParameterizedString;
|
|||||||
import com.google.gerrit.extensions.client.AccountFieldName;
|
import com.google.gerrit.extensions.client.AccountFieldName;
|
||||||
import com.google.gerrit.extensions.client.AuthType;
|
import com.google.gerrit.extensions.client.AuthType;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.account.AbstractRealm;
|
import com.google.gerrit.server.account.AbstractRealm;
|
||||||
import com.google.gerrit.server.account.AccountException;
|
import com.google.gerrit.server.account.AccountException;
|
||||||
import com.google.gerrit.server.account.AuthRequest;
|
import com.google.gerrit.server.account.AuthRequest;
|
||||||
import com.google.gerrit.server.account.EmailExpander;
|
import com.google.gerrit.server.account.EmailExpander;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.account.GroupBackends;
|
import com.google.gerrit.server.account.GroupBackends;
|
||||||
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
|
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
@@ -329,8 +329,12 @@ class LdapRealm extends AbstractRealm {
|
|||||||
public Optional<Account.Id> load(String username) throws Exception {
|
public Optional<Account.Id> load(String username) throws Exception {
|
||||||
try (ReviewDb db = schema.open()) {
|
try (ReviewDb db = schema.open()) {
|
||||||
return Optional.ofNullable(
|
return Optional.ofNullable(
|
||||||
db.accountExternalIds().get(new AccountExternalId.Key(SCHEME_GERRIT, username)))
|
ExternalId.from(
|
||||||
.map(AccountExternalId::getAccountId);
|
db.accountExternalIds()
|
||||||
|
.get(
|
||||||
|
ExternalId.Key.create(SCHEME_GERRIT, username)
|
||||||
|
.asAccountExternalIdKey())))
|
||||||
|
.map(ExternalId::accountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.auth.openid;
|
package com.google.gerrit.server.auth.openid;
|
||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
|
|
||||||
public class OpenIdProviderPattern {
|
public class OpenIdProviderPattern {
|
||||||
public static OpenIdProviderPattern create(String pattern) {
|
public static OpenIdProviderPattern create(String pattern) {
|
||||||
@@ -33,8 +33,8 @@ public class OpenIdProviderPattern {
|
|||||||
return regex ? id.matches(pattern) : id.startsWith(pattern);
|
return regex ? id.matches(pattern) : id.startsWith(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(AccountExternalId id) {
|
public boolean matches(ExternalId extId) {
|
||||||
return matches(id.getExternalId());
|
return matches(extId.key().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -14,9 +14,13 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.config;
|
package com.google.gerrit.server.config;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID;
|
||||||
|
|
||||||
import com.google.gerrit.extensions.client.AuthType;
|
import com.google.gerrit.extensions.client.AuthType;
|
||||||
import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
|
import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
|
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
|
||||||
import com.google.gwtjsonrpc.server.SignedToken;
|
import com.google.gwtjsonrpc.server.SignedToken;
|
||||||
import com.google.gwtjsonrpc.server.XsrfException;
|
import com.google.gwtjsonrpc.server.XsrfException;
|
||||||
@@ -44,7 +48,6 @@ public class AuthConfig {
|
|||||||
private final boolean trustContainerAuth;
|
private final boolean trustContainerAuth;
|
||||||
private final boolean enableRunAs;
|
private final boolean enableRunAs;
|
||||||
private final boolean userNameToLowerCase;
|
private final boolean userNameToLowerCase;
|
||||||
private final boolean gitBasicAuth;
|
|
||||||
private final boolean useContributorAgreements;
|
private final boolean useContributorAgreements;
|
||||||
private final String loginUrl;
|
private final String loginUrl;
|
||||||
private final String loginText;
|
private final String loginText;
|
||||||
@@ -88,7 +91,6 @@ public class AuthConfig {
|
|||||||
cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
|
cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
|
||||||
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
|
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
|
||||||
enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
|
enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
|
||||||
gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
|
|
||||||
gitBasicAuthPolicy = getBasicAuthPolicy(cfg);
|
gitBasicAuthPolicy = getBasicAuthPolicy(cfg);
|
||||||
useContributorAgreements = cfg.getBoolean("auth", "contributoragreements", false);
|
useContributorAgreements = cfg.getBoolean("auth", "contributoragreements", false);
|
||||||
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
|
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
|
||||||
@@ -223,11 +225,6 @@ public class AuthConfig {
|
|||||||
return userNameToLowerCase;
|
return userNameToLowerCase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether git-over-http should use Gerrit basic authentication scheme. */
|
|
||||||
public boolean isGitBasicAuth() {
|
|
||||||
return gitBasicAuth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GitBasicAuthPolicy getGitBasicAuthPolicy() {
|
public GitBasicAuthPolicy getGitBasicAuthPolicy() {
|
||||||
return gitBasicAuthPolicy;
|
return gitBasicAuthPolicy;
|
||||||
}
|
}
|
||||||
@@ -237,7 +234,7 @@ public class AuthConfig {
|
|||||||
return useContributorAgreements;
|
return useContributorAgreements;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
|
public boolean isIdentityTrustable(Collection<ExternalId> ids) {
|
||||||
switch (getAuthType()) {
|
switch (getAuthType()) {
|
||||||
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
|
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
|
||||||
case HTTP:
|
case HTTP:
|
||||||
@@ -258,7 +255,7 @@ public class AuthConfig {
|
|||||||
case OPENID:
|
case OPENID:
|
||||||
// All identities must be trusted in order to trust the account.
|
// All identities must be trusted in order to trust the account.
|
||||||
//
|
//
|
||||||
for (final AccountExternalId e : ids) {
|
for (ExternalId e : ids) {
|
||||||
if (!isTrusted(e)) {
|
if (!isTrusted(e)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -272,8 +269,8 @@ public class AuthConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTrusted(final AccountExternalId id) {
|
private boolean isTrusted(ExternalId id) {
|
||||||
if (id.isScheme(AccountExternalId.SCHEME_MAILTO)) {
|
if (id.isScheme(SCHEME_MAILTO)) {
|
||||||
// mailto identities are created by sending a unique validation
|
// mailto identities are created by sending a unique validation
|
||||||
// token to the address and asking them to come back to the site
|
// token to the address and asking them to come back to the site
|
||||||
// with that token.
|
// with that token.
|
||||||
@@ -281,20 +278,20 @@ public class AuthConfig {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.isScheme(AccountExternalId.SCHEME_UUID)) {
|
if (id.isScheme(SCHEME_UUID)) {
|
||||||
// UUID identities are absolutely meaningless and cannot be
|
// UUID identities are absolutely meaningless and cannot be
|
||||||
// constructed through any normal login process we use.
|
// constructed through any normal login process we use.
|
||||||
//
|
//
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.isScheme(AccountExternalId.SCHEME_USERNAME)) {
|
if (id.isScheme(SCHEME_USERNAME)) {
|
||||||
// We can trust their username, its local to our server only.
|
// We can trust their username, its local to our server only.
|
||||||
//
|
//
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final OpenIdProviderPattern p : trustedOpenIDs) {
|
for (OpenIdProviderPattern p : trustedOpenIDs) {
|
||||||
if (p.matches(id)) {
|
if (p.matches(id)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -30,6 +30,7 @@ import com.google.inject.Inject;
|
|||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
|
public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
|
||||||
@@ -54,7 +55,7 @@ public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
|
|||||||
@Override
|
@Override
|
||||||
public Response<?> apply(ConfigResource rsrc, Input input)
|
public Response<?> apply(ConfigResource rsrc, Input input)
|
||||||
throws AuthException, UnprocessableEntityException, AccountException, OrmException,
|
throws AuthException, UnprocessableEntityException, AccountException, OrmException,
|
||||||
IOException {
|
IOException, ConfigInvalidException {
|
||||||
CurrentUser user = self.get();
|
CurrentUser user = self.get();
|
||||||
if (!user.isIdentifiedUser()) {
|
if (!user.isIdentifiedUser()) {
|
||||||
throw new AuthException("Authentication required");
|
throw new AuthException("Authentication required");
|
||||||
|
@@ -156,7 +156,6 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
|
|||||||
info.useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
|
info.useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
|
||||||
info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
|
info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
|
||||||
info.switchAccountUrl = cfg.getSwitchAccountUrl();
|
info.switchAccountUrl = cfg.getSwitchAccountUrl();
|
||||||
info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
|
|
||||||
info.gitBasicAuthPolicy = cfg.getGitBasicAuthPolicy();
|
info.gitBasicAuthPolicy = cfg.getGitBasicAuthPolicy();
|
||||||
|
|
||||||
if (info.useContributorAgreements != null) {
|
if (info.useContributorAgreements != null) {
|
||||||
|
@@ -110,7 +110,8 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
|
|||||||
String name = ref.getName();
|
String name = ref.getName();
|
||||||
Change.Id changeId;
|
Change.Id changeId;
|
||||||
Account.Id accountId;
|
Account.Id accountId;
|
||||||
if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) {
|
if (name.startsWith(REFS_CACHE_AUTOMERGE)
|
||||||
|
|| (!showMetadata && isMetadata(projectCtl, name))) {
|
||||||
continue;
|
continue;
|
||||||
} else if (RefNames.isRefsEdit(name)) {
|
} else if (RefNames.isRefsEdit(name)) {
|
||||||
// Edits are visible only to the owning user, if change is visible.
|
// Edits are visible only to the owning user, if change is visible.
|
||||||
@@ -138,6 +139,12 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
|
|||||||
if (viewMetadata) {
|
if (viewMetadata) {
|
||||||
result.put(name, ref);
|
result.put(name, ref);
|
||||||
}
|
}
|
||||||
|
} else if (projectCtl.getProjectState().isAllUsers()
|
||||||
|
&& name.equals(RefNames.REFS_EXTERNAL_IDS)) {
|
||||||
|
// The notes branch with the external IDs of all users must not be exposed to normal users.
|
||||||
|
if (viewMetadata) {
|
||||||
|
result.put(name, ref);
|
||||||
|
}
|
||||||
} else if (projectCtl.controlForRef(ref.getLeaf().getName()).isVisible()) {
|
} else if (projectCtl.controlForRef(ref.getLeaf().getName()).isVisible()) {
|
||||||
// Use the leaf to lookup the control data. If the reference is
|
// Use the leaf to lookup the control data. If the reference is
|
||||||
// symbolic we want the control around the final target. If its
|
// symbolic we want the control around the final target. If its
|
||||||
@@ -264,8 +271,10 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isMetadata(String name) {
|
private static boolean isMetadata(ProjectControl projectCtl, String name) {
|
||||||
return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name);
|
return name.startsWith(REFS_CHANGES)
|
||||||
|
|| RefNames.isRefsEdit(name)
|
||||||
|
|| (projectCtl.getProjectState().isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isTag(Ref ref) {
|
private static boolean isTag(Ref ref) {
|
||||||
|
@@ -134,7 +134,8 @@ public class CommitValidators {
|
|||||||
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
|
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
|
||||||
new ConfigValidator(refControl, repo, allUsers),
|
new ConfigValidator(refControl, repo, allUsers),
|
||||||
new BannedCommitsValidator(rejectCommits),
|
new BannedCommitsValidator(rejectCommits),
|
||||||
new PluginCommitValidationListener(pluginValidators)));
|
new PluginCommitValidationListener(pluginValidators),
|
||||||
|
new BlockExternalIdUpdateListener(allUsers)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +150,8 @@ public class CommitValidators {
|
|||||||
new ChangeIdValidator(
|
new ChangeIdValidator(
|
||||||
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
|
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
|
||||||
new ConfigValidator(refControl, repo, allUsers),
|
new ConfigValidator(refControl, repo, allUsers),
|
||||||
new PluginCommitValidationListener(pluginValidators)));
|
new PluginCommitValidationListener(pluginValidators),
|
||||||
|
new BlockExternalIdUpdateListener(allUsers)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommitValidators forMergedCommits(RefControl refControl) {
|
private CommitValidators forMergedCommits(RefControl refControl) {
|
||||||
@@ -617,6 +619,25 @@ public class CommitValidators {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Blocks any update to refs/meta/external-ids */
|
||||||
|
public static class BlockExternalIdUpdateListener implements CommitValidationListener {
|
||||||
|
private final AllUsersName allUsers;
|
||||||
|
|
||||||
|
public BlockExternalIdUpdateListener(AllUsersName allUsers) {
|
||||||
|
this.allUsers = allUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
|
||||||
|
throws CommitValidationException {
|
||||||
|
if (allUsers.equals(receiveEvent.project.getNameKey())
|
||||||
|
&& RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) {
|
||||||
|
throw new CommitValidationException("not allowed to update " + RefNames.REFS_EXTERNAL_IDS);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static CommitValidationMessage getInvalidEmailError(
|
private static CommitValidationMessage getInvalidEmailError(
|
||||||
RevCommit c,
|
RevCommit c,
|
||||||
String type,
|
String type,
|
||||||
|
@@ -18,8 +18,8 @@ import com.google.common.base.Predicates;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.FluentIterable;
|
import com.google.common.collect.FluentIterable;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.index.FieldDef;
|
import com.google.gerrit.server.index.FieldDef;
|
||||||
import com.google.gerrit.server.index.FieldType;
|
import com.google.gerrit.server.index.FieldType;
|
||||||
import com.google.gerrit.server.index.SchemaUtil;
|
import com.google.gerrit.server.index.SchemaUtil;
|
||||||
@@ -42,7 +42,7 @@ public class AccountField {
|
|||||||
new FieldDef.Repeatable<AccountState, String>("external_id", FieldType.EXACT, false) {
|
new FieldDef.Repeatable<AccountState, String>("external_id", FieldType.EXACT, false) {
|
||||||
@Override
|
@Override
|
||||||
public Iterable<String> get(AccountState input, FillArgs args) {
|
public Iterable<String> get(AccountState input, FillArgs args) {
|
||||||
return Iterables.transform(input.getExternalIds(), id -> id.getKey().get());
|
return Iterables.transform(input.getExternalIds(), id -> id.key().get());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,8 +54,7 @@ public class AccountField {
|
|||||||
String fullName = input.getAccount().getFullName();
|
String fullName = input.getAccount().getFullName();
|
||||||
Set<String> parts =
|
Set<String> parts =
|
||||||
SchemaUtil.getNameParts(
|
SchemaUtil.getNameParts(
|
||||||
fullName,
|
fullName, Iterables.transform(input.getExternalIds(), ExternalId::email));
|
||||||
Iterables.transform(input.getExternalIds(), AccountExternalId::getEmailAddress));
|
|
||||||
|
|
||||||
// Additional values not currently added by getPersonParts.
|
// Additional values not currently added by getPersonParts.
|
||||||
// TODO(dborowitz): Move to getPersonParts and remove this hack.
|
// TODO(dborowitz): Move to getPersonParts and remove this hack.
|
||||||
@@ -87,7 +86,7 @@ public class AccountField {
|
|||||||
@Override
|
@Override
|
||||||
public Iterable<String> get(AccountState input, FillArgs args) {
|
public Iterable<String> get(AccountState input, FillArgs args) {
|
||||||
return FluentIterable.from(input.getExternalIds())
|
return FluentIterable.from(input.getExternalIds())
|
||||||
.transform(AccountExternalId::getEmailAddress)
|
.transform(ExternalId::email)
|
||||||
.append(Collections.singleton(input.getAccount().getPreferredEmail()))
|
.append(Collections.singleton(input.getAccount().getPreferredEmail()))
|
||||||
.filter(Predicates.notNull())
|
.filter(Predicates.notNull())
|
||||||
.transform(String::toLowerCase)
|
.transform(String::toLowerCase)
|
||||||
|
@@ -18,6 +18,7 @@ import com.google.common.base.Joiner;
|
|||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.index.IndexConfig;
|
import com.google.gerrit.server.index.IndexConfig;
|
||||||
import com.google.gerrit.server.index.account.AccountIndexCollection;
|
import com.google.gerrit.server.index.account.AccountIndexCollection;
|
||||||
import com.google.gerrit.server.query.InternalQuery;
|
import com.google.gerrit.server.query.InternalQuery;
|
||||||
@@ -67,11 +68,27 @@ public class InternalAccountQuery extends InternalQuery<AccountState> {
|
|||||||
return query(AccountPredicates.defaultPredicate(query));
|
return query(AccountPredicates.defaultPredicate(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AccountState> byExternalId(String externalId) throws OrmException {
|
public List<AccountState> byEmailPrefix(String emailPrefix) throws OrmException {
|
||||||
return query(AccountPredicates.externalId(externalId));
|
return query(AccountPredicates.email(emailPrefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
|
||||||
|
return byExternalId(ExternalId.Key.create(scheme, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException {
|
||||||
|
return query(AccountPredicates.externalId(externalId.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountState oneByExternalId(String externalId) throws OrmException {
|
public AccountState oneByExternalId(String externalId) throws OrmException {
|
||||||
|
return oneByExternalId(ExternalId.Key.parse(externalId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountState oneByExternalId(String scheme, String id) throws OrmException {
|
||||||
|
return oneByExternalId(ExternalId.Key.create(scheme, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountState oneByExternalId(ExternalId.Key externalId) throws OrmException {
|
||||||
List<AccountState> accountStates = byExternalId(externalId);
|
List<AccountState> accountStates = byExternalId(externalId);
|
||||||
if (accountStates.size() == 1) {
|
if (accountStates.size() == 1) {
|
||||||
return accountStates.get(0);
|
return accountStates.get(0);
|
||||||
|
@@ -56,6 +56,18 @@ public class AclUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void block(
|
||||||
|
ProjectConfig config, AccessSection section, String permission, GroupReference... groupList) {
|
||||||
|
Permission p = section.getPermission(permission, true);
|
||||||
|
for (GroupReference group : groupList) {
|
||||||
|
if (group != null) {
|
||||||
|
PermissionRule r = rule(config, group);
|
||||||
|
r.setBlock();
|
||||||
|
p.add(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void grant(
|
public static void grant(
|
||||||
ProjectConfig config,
|
ProjectConfig config,
|
||||||
AccessSection section,
|
AccessSection section,
|
||||||
|
@@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
/** A version of the database schema. */
|
/** A version of the database schema. */
|
||||||
public abstract class SchemaVersion {
|
public abstract class SchemaVersion {
|
||||||
/** The current schema version. */
|
/** 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() {
|
public static int getBinaryVersion() {
|
||||||
return guessVersion(C);
|
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
|
exit 1
|
||||||
fi
|
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
|
$server/a/changes/$changeId/revisions/current/preview_submit?format=tgz >http_code
|
||||||
if ! grep 200 http_code >/dev/null
|
if ! grep 200 http_code >/dev/null
|
||||||
then
|
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();
|
||||||
|
}
|
||||||
|
}
|
@@ -23,18 +23,13 @@ import static org.easymock.EasyMock.verify;
|
|||||||
|
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
|
||||||
import com.google.gerrit.server.account.AccountCache;
|
import com.google.gerrit.server.account.AccountCache;
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
import com.google.gerrit.server.account.WatchConfig.NotifyType;
|
|
||||||
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
|
|
||||||
import com.google.gerrit.server.mail.Address;
|
import com.google.gerrit.server.mail.Address;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import org.eclipse.jgit.lib.Config;
|
import org.eclipse.jgit.lib.Config;
|
||||||
import org.eclipse.jgit.lib.PersonIdent;
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -388,9 +383,6 @@ public class FromAddressGeneratorProviderTest {
|
|||||||
account.setFullName(name);
|
account.setFullName(name);
|
||||||
account.setPreferredEmail(email);
|
account.setPreferredEmail(email);
|
||||||
return new AccountState(
|
return new AccountState(
|
||||||
account,
|
account, Collections.emptySet(), Collections.emptySet(), new HashMap<>());
|
||||||
Collections.<AccountGroup.UUID>emptySet(),
|
|
||||||
Collections.<AccountExternalId>emptySet(),
|
|
||||||
new HashMap<ProjectWatchKey, Set<NotifyType>>());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,15 +17,10 @@ package com.google.gerrit.testutil;
|
|||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
|
||||||
import com.google.gerrit.server.account.AccountCache;
|
import com.google.gerrit.server.account.AccountCache;
|
||||||
import com.google.gerrit.server.account.AccountState;
|
import com.google.gerrit.server.account.AccountState;
|
||||||
import com.google.gerrit.server.account.WatchConfig.NotifyType;
|
|
||||||
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/** Fake implementation of {@link AccountCache} for testing. */
|
/** Fake implementation of {@link AccountCache} for testing. */
|
||||||
public class FakeAccountCache implements AccountCache {
|
public class FakeAccountCache implements AccountCache {
|
||||||
@@ -81,10 +76,6 @@ public class FakeAccountCache implements AccountCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static AccountState newState(Account account) {
|
private static AccountState newState(Account account) {
|
||||||
return new AccountState(
|
return new AccountState(account, ImmutableSet.of(), ImmutableSet.of(), new HashMap<>());
|
||||||
account,
|
|
||||||
ImmutableSet.<AccountGroup.UUID>of(),
|
|
||||||
ImmutableSet.<AccountExternalId>of(),
|
|
||||||
new HashMap<ProjectWatchKey, Set<NotifyType>>());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,13 +14,13 @@
|
|||||||
|
|
||||||
package com.google.gerrit.sshd;
|
package com.google.gerrit.sshd;
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
|
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
|
||||||
|
|
||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import com.google.gerrit.reviewdb.client.AccountExternalId;
|
|
||||||
import com.google.gerrit.reviewdb.client.AccountSshKey;
|
import com.google.gerrit.reviewdb.client.AccountSshKey;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.account.ExternalId;
|
||||||
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
|
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
|
||||||
import com.google.gerrit.server.cache.CacheModule;
|
import com.google.gerrit.server.cache.CacheModule;
|
||||||
import com.google.gerrit.server.ssh.SshKeyCache;
|
import com.google.gerrit.server.ssh.SshKeyCache;
|
||||||
@@ -103,14 +103,17 @@ public class SshKeyCacheImpl implements SshKeyCache {
|
|||||||
@Override
|
@Override
|
||||||
public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
|
public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
|
||||||
try (ReviewDb db = schema.open()) {
|
try (ReviewDb db = schema.open()) {
|
||||||
AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_USERNAME, username);
|
ExternalId user =
|
||||||
AccountExternalId user = db.accountExternalIds().get(key);
|
ExternalId.from(
|
||||||
|
db.accountExternalIds()
|
||||||
|
.get(
|
||||||
|
ExternalId.Key.create(SCHEME_USERNAME, username).asAccountExternalIdKey()));
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return NO_SUCH_USER;
|
return NO_SUCH_USER;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SshKeyCacheEntry> kl = new ArrayList<>(4);
|
List<SshKeyCacheEntry> kl = new ArrayList<>(4);
|
||||||
for (AccountSshKey k : authorizedKeys.getKeys(user.getAccountId())) {
|
for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) {
|
||||||
if (k.isValid()) {
|
if (k.isValid()) {
|
||||||
add(kl, k);
|
add(kl, k);
|
||||||
}
|
}
|
||||||
|
@@ -263,7 +263,7 @@ final class SetAccountCommand extends SshCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addEmail(String email)
|
private void addEmail(String email)
|
||||||
throws UnloggedFailure, RestApiException, OrmException, IOException {
|
throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException {
|
||||||
EmailInput in = new EmailInput();
|
EmailInput in = new EmailInput();
|
||||||
in.email = email;
|
in.email = email;
|
||||||
in.noConfirmation = true;
|
in.noConfirmation = true;
|
||||||
@@ -274,7 +274,8 @@ final class SetAccountCommand extends SshCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteEmail(String email) throws RestApiException, OrmException, IOException {
|
private void deleteEmail(String email)
|
||||||
|
throws RestApiException, OrmException, IOException, ConfigInvalidException {
|
||||||
if (email.equals("ALL")) {
|
if (email.equals("ALL")) {
|
||||||
List<EmailInfo> emails = getEmails.apply(rsrc);
|
List<EmailInfo> emails = getEmails.apply(rsrc);
|
||||||
for (EmailInfo e : emails) {
|
for (EmailInfo e : emails) {
|
||||||
|
Submodule plugins/cookbook-plugin updated: 85083bc964...e6d7594621
Reference in New Issue
Block a user