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:
ekempin
2017-02-28 09:03:53 +00:00
committed by Gerrit Code Review
90 changed files with 2380 additions and 1149 deletions

View File

@@ -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`
+ +

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`

View File

@@ -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);

View File

@@ -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;
} }
} }

View File

@@ -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();
}
} }
} }

View File

@@ -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();

View File

@@ -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;
} }

View File

@@ -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()));
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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()) {

View File

@@ -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());
} }

View File

@@ -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());

View File

@@ -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();
} }
} }

View File

@@ -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);

View File

@@ -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()) {

View File

@@ -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());
} }

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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",
], ],
) )

View File

@@ -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()

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();
} }
} }
} }

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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

View File

@@ -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);
}
} }

View File

@@ -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/";

View File

@@ -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",
], ],
) )

View File

@@ -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);
} }
} }

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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());
} }
} }

View File

@@ -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());
}
} }

View File

@@ -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;

View File

@@ -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();
} }
} }

View File

@@ -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;
} }

View File

@@ -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;
}
} }

View File

@@ -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);
}
} }

View File

@@ -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");
} }

View File

@@ -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);
} }

View File

@@ -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());
} }
} }

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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);

View File

@@ -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);

View File

@@ -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");
} }

View File

@@ -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);
} }
} }

View File

@@ -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);
} }

View File

@@ -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();
}
}
} }

View File

@@ -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);
} }
} }

View File

@@ -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;

View File

@@ -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);
} }
} }
} }

View File

@@ -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

View File

@@ -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;
} }

View File

@@ -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");

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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>>());
} }
} }

View File

@@ -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>>());
} }
} }

View File

@@ -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);
} }

View File

@@ -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) {