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
commit 76553082d3
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
<<ldap.username,ldap.username>> identity. Gerrit can also use kerberos if
<<ldap.authentication,ldap.authentication>> is set to `GSSAPI`.
+
If link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP`,
the randomly generated HTTP password is used for authentication. On the other hand,
if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
the password in the request is first checked against the HTTP password and, if
it does not match, it is then validated against the LDAP password.
Service users that only exist in the Gerrit database are authenticated by their
HTTP passwords.
* `LDAP_BIND`
+
@ -164,6 +172,12 @@ types of data, and can be revoked by users at any time.
Site owners have to register their application before getting started. Note
that provider specific plugins must be used with this authentication scheme.
+
Git clients may send OAuth 2 access tokens instead of passwords in the Basic
authentication header. Note that provider specific plugins must be installed to
facilitate this authentication scheme. If multiple OAuth 2 provider plugins are
installed one of them must be selected as default with the
`auth.gitOAuthProvider` option.
+
* `DEVELOPMENT_BECOME_ANY_ACCOUNT`
+
*DO NOT USE*. Only for use in a development environment.
@ -279,7 +293,7 @@ The "Sign In" link will send users directly to this URL.
[[auth.httpHeader]]auth.httpHeader::
+
HTTP header to trust the username from, or unset to select HTTP basic
or digest authentication. Only used if `auth.type` is set to `HTTP`.
authentication. Only used if `auth.type` is set to `HTTP`.
[[auth.httpDisplaynameHeader]]auth.httpDisplaynameHeader::
+
@ -445,45 +459,16 @@ Gerrit to authenticate users. In this case Gerrit will blindly trust
the container.
+
This parameter only affects git over http traffic. If set to false
then Gerrit will do the authentication (using DIGEST authentication).
then Gerrit will do the authentication (using Basic authentication).
+
By default this is set to false.
[[auth.gitBasicAuth]]auth.gitBasicAuth::
+
If true then Git over HTTP and HTTP/S traffic is authenticated using
standard BasicAuth. Depending on the configured `auth.type`, credentials
are validated against the randomly generated HTTP password, against LDAP
(`auth.type = LDAP`) or against an OAuth 2 provider (`auth.type = OAUTH`).
+
This parameter affects git over HTTP traffic and access to the REST
API. If set to false then Gerrit will authenticate through DIGEST
authentication and the randomly generated HTTP password in the Gerrit
database.
+
When `auth.type` is `LDAP`, users should authenticate using their LDAP passwords.
However, if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP`,
the randomly generated HTTP password is used exclusively. In the other hand,
if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
the password in the request is first checked against the HTTP password and, if
it does not match, it is then validated against the LDAP password.
Service users that only exist in the Gerrit database are authenticated by their
HTTP passwords.
+
When `auth.type` is `OAUTH`, Git clients may send OAuth 2 access tokens
instead of passwords in the Basic authentication header. Note that provider
specific plugins must be installed to facilitate this authentication scheme.
If multiple OAuth 2 provider plugins are installed one of them must be
selected as default with the `auth.gitOAuthProvider` option.
+
By default this is set to false.
[[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
+
When `auth.type` is `LDAP` and BasicAuth (i.e., link:#auth.gitBasicAuth[`auth.gitBasicAuth`]
is set to true), it allows using either the generated HTTP password, the LDAP
password or both to authenticate Git over HTTP and REST API requests. The
supported values are:
When `auth.type` is `LDAP`, it allows using either the generated HTTP password,
the LDAP password, or both, to authenticate Git over HTTP and REST API
requests. The supported values are:
+
*`HTTP`
+

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
successful.
== HTTP Basic/Digest Authentication
== HTTP Basic Authentication
When using HTTP authentication, Gerrit assumes that the servlet
container or the frontend web server has performed all user

View File

@ -1443,7 +1443,7 @@ can be accessed from any REST client, i. e.:
----
curl -X POST -H "Content-Type: application/json" \
-d '{message: "François", french: true}' \
--digest --user joe:secret \
--user joe:secret \
http://host:port/a/changes/1/revisions/1/cookbook~say-hello
"Bonjour François from change 1, patch set 1!"
----
@ -2451,18 +2451,18 @@ is an error. Errors are always handled by the Gerrit core UI which
shows the error dialog. This means currently plugins cannot do any
error handling and e.g. ignore expected errors.
In the following example the REST endpoint would return '404 Not Found'
if there is no HTTP password and the Gerrit core UI would display an
error dialog for this. However having no HTTP password is not an error
and the plugin may like to handle this case.
In the following example the REST endpoint would return '404 Not
Found' if the user has no username and the Gerrit core UI would
display an error dialog for this. However having no username is
not an error and the plugin may like to handle this case.
[source,java]
----
new RestApi("accounts").id("self").view("password.http")
new RestApi("accounts").id("self").view("username")
.get(new AsyncCallback<NativeString>() {
@Override
public void onSuccess(NativeString httpPassword) {
public void onSuccess(NativeString username) {
// TODO
}

View File

@ -47,7 +47,7 @@ option instead:
Example to set a Gerrit project's link:rest-api-projects.html#set-project-description[description]:
----
curl -X PUT --digest --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json; charset=UTF-8" http://localhost:8080/a/projects/myproject/description
curl -X PUT --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json; charset=UTF-8" http://localhost:8080/a/projects/myproject/description
----
=== Authentication
@ -56,7 +56,7 @@ To test APIs that require authentication, the username and password must be spec
the command line:
----
curl --digest --user username:password http://localhost:8080/a/path/to/api/
curl --user username:password http://localhost:8080/a/path/to/api/
----
This makes it easy to switch users for testing of permissions.
@ -65,7 +65,7 @@ It is also possible to test with a username and password from the `.netrc`
file (on Windows, `_netrc`):
----
curl --digest -n http://localhost:8080/a/path/to/api/
curl -n http://localhost:8080/a/path/to/api/
----
In both cases, the password should be the user's link:user-upload.html#http[HTTP password].
@ -75,7 +75,7 @@ In both cases, the password should be the user's link:user-upload.html#http[HTTP
To verify the headers returned from a REST API call, use `curl` in verbose mode:
----
curl -v -n --digest -X DELETE http://localhost:8080/a/path/to/api/
curl -v -n -X DELETE http://localhost:8080/a/path/to/api/
----
The headers on both the request and the response will be printed.

View File

@ -9,7 +9,6 @@ account to lower case
--
_java_ -jar gerrit.war _LocalUsernamesToLowerCase
-d <SITE_PATH>
[--threads]
--
== DESCRIPTION
@ -40,10 +39,6 @@ must be run by itself.
Location of the gerrit.config file, and all other per-site
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
This command can only be run on a server which has direct
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`".
[[get-http-password]]
=== Get HTTP Password
--
'GET /accounts/link:#account-id[\{account-id\}]/password.http'
--
Retrieves the HTTP password of an account.
.Request
----
GET /accounts/john.doe@example.com/password.http HTTP/1.0
----
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
"Qmxlc21ydCB1YmVyIGFsbGVzIGluIGRlciBXZWx0IQ"
----
If the account does not have an HTTP password the response is "`404 Not Found`".
[[set-http-password]]
=== Set/Generate HTTP Password
--
@ -1028,12 +1003,12 @@ link:#capability-info[CapabilityInfo] entity.
}
----
Administrator that has authenticated with digest authentication:
Administrator that has authenticated with basic authentication:
.Request
----
GET /a/accounts/self/capabilities HTTP/1.0
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
Authorization: Basic ABCDECF..
----
.Response
@ -1075,7 +1050,7 @@ possible alternative for the caller.
.Request
----
GET /a/accounts/self/capabilities?q=createAccount&q=createGroup HTTP/1.0
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
Authorization: Basic ABCDEF...
----
.Response

View File

@ -470,9 +470,9 @@ The cache names are lexicographically sorted.
E.g. this could be used to flush all caches:
+
----
for c in $(curl --digest --user jdoe:TNAuLkXsIV7w http://gerrit/a/config/server/caches/?format=TEXT_LIST | base64 -D)
for c in $(curl --user jdoe:TNAuLkXsIV7w http://gerrit/a/config/server/caches/?format=TEXT_LIST | base64 -D)
do
curl --digest --user jdoe:TNAuLkXsIV7w -X POST http://gerrit/a/config/server/caches/$c/flush
curl --user jdoe:TNAuLkXsIV7w -X POST http://gerrit/a/config/server/caches/$c/flush
done
----
@ -1270,11 +1270,6 @@ type] is `LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
The link:config-gerrit.html#auth.httpPasswordUrl[URL to obtain an HTTP
password]. Only set if link:config-gerrit.html#auth.type[authentication
type] is `CUSTOM_EXTENSION`.
|`is_git_basic_auth` |optional, not set if `false`|
Whether link:config-gerrit.html#auth.gitBasicAuth[basic authentication
is used for Git over HTTP/HTTPS]. Only set if
link:config-gerrit.html#auth.type[authentication type] is is `LDAP` or
`LDAP_BIND`.
|`git_basic_auth_policy` |optional|
The link:config-gerrit.html#auth.gitBasicAuthPolicy[policy] to authenticate
Git over HTTP and REST API requests when

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

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/projects/`.
By default Gerrit uses HTTP digest authentication with the HTTP password
from the user's account settings page. HTTP basic authentication is used
if link:config-gerrit.html#auth.gitBasicAuth[`auth.gitBasicAuth`] is set
to true in the Gerrit configuration.
Gerrit uses HTTP basic authentication with the HTTP password from the
user's account settings page.
[[preconditions]]
=== Preconditions

View File

@ -18,10 +18,9 @@ public key, and HTTP/HTTPS.
On Gerrit installations that do not support SSH authentication, the
user must authenticate via HTTP/HTTPS.
When link:config-gerrit.html#auth.gitBasicAuth[gitBasicAuth] is enabled,
the user is authenticated using standard BasicAuth. Depending on the value of
link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy], credentials are
validated using:
The user is authenticated using standard BasicAuth. Depending on the
value of link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy],
credentials are validated using:
* The randomly generated HTTP password on the `HTTP Password` tab
in the user settings page if `gitBasicAuthPolicy` is `HTTP`.
@ -29,9 +28,10 @@ validated using:
* Both, the HTTP and the LDAP passwords (in this order) if `gitBasicAuthPolicy`
is `HTTP_LDAP`.
When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can be
accessed within Gerrit by going to `Settings`, and then accessing the `HTTP
Password` tab.
When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can
be regenerated by going to `Settings`, and then accessing the `HTTP
Password` tab. Revocation can effectively be done by regenerating the
password and then forgetting it.
For Gerrit installations where an link:config-gerrit.html#auth.httpPasswordUrl[HTTP password URL]
is configured, the password can be obtained by clicking on `Obtain Password`

View File

@ -20,12 +20,13 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.gerrit.common.TimeUtil;
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.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.index.account.AccountIndexer;
@ -39,8 +40,10 @@ import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.KeyPair;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Singleton
@ -54,6 +57,7 @@ public class AccountCreator {
private final AccountCache accountCache;
private final AccountByEmailCache byEmailCache;
private final AccountIndexer indexer;
private final ExternalIdsUpdate.Server externalIdsUpdate;
@Inject
AccountCreator(
@ -63,7 +67,8 @@ public class AccountCreator {
SshKeyCache sshKeyCache,
AccountCache accountCache,
AccountByEmailCache byEmailCache,
AccountIndexer indexer) {
AccountIndexer indexer,
ExternalIdsUpdate.Server externalIdsUpdate) {
accounts = new HashMap<>();
reviewDbProvider = schema;
this.authorizedKeys = authorizedKeys;
@ -72,6 +77,7 @@ public class AccountCreator {
this.accountCache = accountCache;
this.byEmailCache = byEmailCache;
this.indexer = indexer;
this.externalIdsUpdate = externalIdsUpdate;
}
public synchronized TestAccount create(
@ -83,18 +89,14 @@ public class AccountCreator {
try (ReviewDb db = reviewDbProvider.open()) {
Account.Id id = new Account.Id(db.nextAccountId());
AccountExternalId extUser =
new AccountExternalId(
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
List<ExternalId> extIds = new ArrayList<>(2);
String httpPass = "http-pass";
extUser.setPassword(httpPass);
db.accountExternalIds().insert(Collections.singleton(extUser));
extIds.add(ExternalId.createUsername(username, id, httpPass));
if (email != null) {
AccountExternalId extMailto = new AccountExternalId(id, getEmailKey(email));
extMailto.setEmailAddress(email);
db.accountExternalIds().insert(Collections.singleton(extMailto));
extIds.add(ExternalId.createEmail(id, email));
}
externalIdsUpdate.create().insert(db, extIds);
Account a = new Account(id, TimeUtil.nowTs());
a.setFullName(fullName);
@ -157,10 +159,6 @@ public class AccountCreator {
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 {
JSch jsch = new JSch();
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.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.acceptance.AbstractDaemonTest;
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.testutil.TestKey;
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.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.NotifyType;
import com.google.gerrit.server.config.AllUsersName;
@ -77,10 +80,10 @@ import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.bouncycastle.bcpg.ArmoredOutputStream;
@ -111,10 +114,17 @@ public class AccountIT extends AbstractDaemonTest {
@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
public void saveExternalIds() throws Exception {
externalIdsUpdate = externalIdsUpdateFactory.create();
savedExternalIds = new ArrayList<>();
savedExternalIds.addAll(getExternalIds(admin));
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
// @Before in AbstractDaemonTest prevents this class' @Before method from
// being executed.
db.accountExternalIds().delete(getExternalIds(admin));
db.accountExternalIds().delete(getExternalIds(user));
db.accountExternalIds().insert(savedExternalIds);
externalIdsUpdate.delete(db, getExternalIds(admin));
externalIdsUpdate.delete(db, getExternalIds(user));
externalIdsUpdate.insert(db, savedExternalIds);
}
accountCache.evict(admin.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();
}
@ -440,11 +450,11 @@ public class AccountIT extends AbstractDaemonTest {
String email = "foo.bar@example.com";
String extId1 = "foo:bar";
String extId2 = "foo:baz";
db.accountExternalIds()
.insert(
ImmutableList.of(
createExternalIdWithEmail(extId1, email),
createExternalIdWithEmail(extId2, email)));
List<ExternalId> extIds =
ImmutableList.of(
ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email),
ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email));
externalIdsUpdateFactory.create().insert(db, extIds);
accountCache.evict(admin.id);
assertThat(
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);
}
@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
public void putStatus() throws Exception {
List<String> statuses = ImmutableList.of("OOO", "Busy");
@ -680,10 +713,7 @@ public class AccountIT extends AbstractDaemonTest {
public void addOtherUsersGpgKey_Conflict() throws Exception {
// Both users have a matching external ID for this key.
addExternalIdEmail(admin, "test5@example.com");
AccountExternalId extId =
new AccountExternalId(user.getId(), new AccountExternalId.Key("foo:myId"));
db.accountExternalIds().insert(Collections.singleton(extId));
externalIdsUpdate.insert(db, ExternalId.create("foo", "myId", user.getId()));
accountCache.evict(user.getId());
TestKey key = validKeyWithSecondUserId();
@ -883,7 +913,7 @@ public class AccountIT extends AbstractDaemonTest {
Iterable<String> expectedFps =
expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
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);
// Check raw stored keys.
@ -908,11 +938,9 @@ public class AccountIT extends AbstractDaemonTest {
private void addExternalIdEmail(TestAccount account, String email) throws Exception {
checkNotNull(email);
AccountExternalId extId =
new AccountExternalId(account.getId(), new AccountExternalId.Key(name("test"), email));
extId.setEmailAddress(email);
db.accountExternalIds().insert(Collections.singleton(extId));
// Clear saved AccountState and AccountExternalIds.
externalIdsUpdate.insert(
db, ExternalId.createWithEmail(name("test"), email, account.getId(), email));
// Clear saved AccountState and ExternalIds.
accountCache.evict(account.getId());
setApiUser(account);
}
@ -932,9 +960,8 @@ public class AccountIT extends AbstractDaemonTest {
return gApi.accounts().self().getEmails().stream().map(e -> e.email).collect(toSet());
}
private AccountExternalId createExternalIdWithEmail(String id, String email) {
AccountExternalId extId = new AccountExternalId(admin.id, new AccountExternalId.Key(id));
extId.setEmailAddress(email);
return extId;
private void assertEmail(Set<Account.Id> accounts, TestAccount expectedAccount) {
assertThat(accounts).hasSize(1);
assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
}
}

View File

@ -15,30 +15,64 @@
package com.google.gerrit.acceptance.rest.account;
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.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
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.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.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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;
@Sandboxed
public class ExternalIdIT extends AbstractDaemonTest {
@Inject private AllUsersName allUsers;
@Inject private ExternalIdsUpdate.Server extIdsUpdate;
@Inject private ExternalIds externalIds;
@Test
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<>();
for (AccountExternalId id : expectedIds) {
id.setCanDelete(!id.getExternalId().equals("username:" + user.username));
id.setTrusted(true);
expectedIdInfos.add(toInfo(id));
for (ExternalId id : expectedIds) {
AccountExternalIdInfo info = new AccountExternalIdInfo();
info.identity = id.key().get();
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");
@ -102,12 +136,119 @@ public class ExternalIdIT extends AbstractDaemonTest {
.isEqualTo(String.format("External id %s does not exist", externalIdStr));
}
private static AccountExternalIdInfo toInfo(AccountExternalId id) {
AccountExternalIdInfo info = new AccountExternalIdInfo();
info.identity = id.getExternalId();
info.emailAddress = id.getEmailAddress();
info.trusted = id.isTrusted() ? true : null;
info.canDelete = id.canDelete() ? true : null;
return info;
@Test
public void fetchExternalIdsBranch() throws Exception {
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
// refs/meta/external-ids is only visible to users with the 'Access Database' capability
try {
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.editFullNameUrl).isNull();
assertThat(i.auth.httpPasswordUrl).isNull();
assertThat(i.auth.isGitBasicAuth).isNull();
// change
assertThat(i.change.allowDrafts).isNull();
@ -163,7 +162,6 @@ public class ServerInfoIT extends AbstractDaemonTest {
assertThat(i.auth.registerText).isNull();
assertThat(i.auth.editFullNameUrl).isNull();
assertThat(i.auth.httpPasswordUrl).isNull();
assertThat(i.auth.isGitBasicAuth).isNull();
// change
assertThat(i.change.allowDrafts).isTrue();

View File

@ -31,6 +31,5 @@ public class AuthInfo {
public String registerText;
public String editFullNameUrl;
public String httpPasswordUrl;
public Boolean isGitBasicAuth;
public GitBasicAuthPolicy gitBasicAuthPolicy;
}

View File

@ -15,16 +15,16 @@
package com.google.gerrit.gpg;
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.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.IdentifiedUser;
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.GerritServerConfig;
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 {
List<AccountState> accountStates =
accountQueryProvider.get().byExternalId(toExtIdKey(key).get());
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key));
if (accountStates.isEmpty()) {
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) {
Set<String> result = new HashSet<>();
result.addAll(user.getEmailAddresses());
for (AccountExternalId extId : user.state().getExternalIds()) {
for (ExternalId extId : user.state().getExternalIds()) {
if (extId.isScheme(SCHEME_GPGKEY)) {
continue; // Omit GPG keys.
}
result.add(extId.getExternalId());
result.add(extId.key().get());
}
return result;
}
@ -248,8 +247,7 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
return sb.toString();
}
static AccountExternalId.Key toExtIdKey(PGPPublicKey key) {
return new AccountExternalId.Key(
SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
static ExternalId.Key toExtIdKey(PGPPublicKey key) {
return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
}
}

View File

@ -33,6 +33,7 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.bouncycastle.openpgp.PGPException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.PushCertificateParser;
@ -78,7 +79,7 @@ public class GpgApiAdapterImpl implements GpgApiAdapter {
in.delete = delete;
try {
return postGpgKeys.apply(account, in);
} catch (PGPException | OrmException | IOException e) {
} catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
throw new GpgException(e);
}
}

View File

@ -25,6 +25,7 @@ import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import org.bouncycastle.openpgp.PGPException;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class GpgKeyApiImpl implements GpgKeyApi {
public interface Factory {
@ -55,7 +56,7 @@ public class GpgKeyApiImpl implements GpgKeyApi {
public void delete() throws RestApiException {
try {
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);
}
}

View File

@ -15,6 +15,7 @@
package com.google.gerrit.gpg.server;
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.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.gpg.PublicKeyStore;
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.server.GerritPersonIdent;
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.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.Collections;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
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<PublicKeyStore> storeProvider;
private final AccountCache accountCache;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@Inject
DeleteGpgKey(
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<ReviewDb> db,
Provider<PublicKeyStore> storeProvider,
AccountCache accountCache) {
AccountCache accountCache,
ExternalIdsUpdate.User externalIdsUpdateFactory) {
this.serverIdent = serverIdent;
this.db = db;
this.storeProvider = storeProvider;
this.accountCache = accountCache;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
}
@Override
public Response<?> apply(GpgKey rsrc, Input input)
throws ResourceConflictException, PGPException, OrmException, IOException {
throws ResourceConflictException, PGPException, OrmException, IOException,
ConfigInvalidException {
PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
AccountExternalId.Key extIdKey =
new AccountExternalId.Key(
AccountExternalId.SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
db.get().accountExternalIds().deleteKeys(Collections.singleton(extIdKey));
externalIdsUpdateFactory
.create()
.delete(
db.get(),
rsrc.getUser().getAccountId(),
ExternalId.Key.create(
SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
accountCache.evict(rsrc.getUser().getAccountId());
try (PublicKeyStore store = storeProvider.get()) {

View File

@ -14,7 +14,7 @@
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 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.PublicKeyStore;
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.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.ExternalId;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@ -114,7 +114,7 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
throw new ResourceNotFoundException(id);
}
static byte[] parseFingerprint(String str, Iterable<AccountExternalId> existingExtIds)
static byte[] parseFingerprint(String str, Iterable<ExternalId> existingExtIds)
throws ResourceNotFoundException {
str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
if ((str.length() != 8 && str.length() != 40)
@ -122,8 +122,8 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
throw new ResourceNotFoundException(str);
}
byte[] fp = null;
for (AccountExternalId extId : existingExtIds) {
String fpStr = extId.getSchemeRest();
for (ExternalId extId : existingExtIds) {
String fpStr = extId.key().id();
if (!fpStr.endsWith(str)) {
continue;
} else if (fp != null) {
@ -152,8 +152,8 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
checkVisible(self, rsrc);
Map<String, GpgKeyInfo> keys = new HashMap<>();
try (PublicKeyStore store = storeProvider.get()) {
for (AccountExternalId extId : getGpgExtIds(rsrc)) {
String fpStr = extId.getSchemeRest();
for (ExternalId extId : getGpgExtIds(rsrc)) {
String fpStr = extId.key().id();
byte[] fp = BaseEncoding.base16().decode(fpStr);
boolean found = false;
for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
@ -199,13 +199,14 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
}
@VisibleForTesting
public static FluentIterable<AccountExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId)
public static FluentIterable<ExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId)
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));
}
private Iterable<AccountExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException {
private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException {
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.keyToString;
import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
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.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
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.server.PostGpgKeys.Input;
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.server.CurrentUser;
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.AccountResource;
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.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
@ -66,6 +68,7 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
@ -88,6 +91,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
private final AddKeySender.Factory addKeyFactory;
private final AccountCache accountCache;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@Inject
PostGpgKeys(
@ -98,7 +102,8 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
GerritPublicKeyChecker.Factory checkerFactory,
AddKeySender.Factory addKeyFactory,
AccountCache accountCache,
Provider<InternalAccountQuery> accountQueryProvider) {
Provider<InternalAccountQuery> accountQueryProvider,
ExternalIdsUpdate.User externalIdsUpdateFactory) {
this.serverIdent = serverIdent;
this.db = db;
this.self = self;
@ -107,48 +112,48 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
this.addKeyFactory = addKeyFactory;
this.accountCache = accountCache;
this.accountQueryProvider = accountQueryProvider;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
}
@Override
public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
PGPException, OrmException, IOException {
PGPException, OrmException, IOException, ConfigInvalidException {
GpgKeys.checkVisible(self, rsrc);
List<AccountExternalId> existingExtIds =
Collection<ExternalId> existingExtIds =
GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
try (PublicKeyStore store = storeProvider.get()) {
Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
List<AccountExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
List<ExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
for (PGPPublicKeyRing keyRing : newKeys) {
PGPPublicKey key = keyRing.getPublicKey();
AccountExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
Account account = getAccountByExternalId(extIdKey.get());
ExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
Account account = getAccountByExternalId(extIdKey);
if (account != null) {
if (!account.getId().equals(rsrc.getUser().getAccountId())) {
throw new ResourceConflictException("GPG key already associated with another account");
}
} else {
newExtIds.add(new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey));
newExtIds.add(ExternalId.create(extIdKey, rsrc.getUser().getAccountId()));
}
}
storeKeys(rsrc, newKeys, toRemove);
if (!newExtIds.isEmpty()) {
db.get().accountExternalIds().insert(newExtIds);
}
db.get()
.accountExternalIds()
.deleteKeys(Iterables.transform(toRemove, fp -> toExtIdKey(fp.get())));
List<ExternalId.Key> extIdKeysToRemove =
toRemove.stream().map(fp -> toExtIdKey(fp.get())).collect(toList());
externalIdsUpdateFactory
.create()
.replace(db.get(), rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
accountCache.evict(rsrc.getUser().getAccountId());
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()) {
return ImmutableSet.of();
}
@ -243,13 +248,12 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
}
}
private AccountExternalId.Key toExtIdKey(byte[] fp) {
return new AccountExternalId.Key(
AccountExternalId.SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
private ExternalId.Key toExtIdKey(byte[] fp) {
return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
}
private Account getAccountByExternalId(String externalId) throws OrmException {
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(externalId);
private Account getAccountByExternalId(ExternalId.Key extIdKey) throws OrmException {
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
if (accountStates.isEmpty()) {
return null;
@ -257,7 +261,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
if (accountStates.size() > 1) {
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(", ")
.appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
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.keyD;
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.FORCED;
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.lifecycle.LifecycleManager;
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.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountManager;
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.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
@ -55,7 +55,6 @@ import com.google.inject.util.Providers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@ -86,6 +85,8 @@ public class GerritPublicKeyCheckerTest {
@Inject private ThreadLocalRequestContext requestContext;
@Inject private ExternalIdsUpdate.Server externalIdsUpdateFactory;
private LifecycleManager lifecycle;
private ReviewDb db;
private Account.Id userId;
@ -221,7 +222,8 @@ public class GerritPublicKeyCheckerTest {
@Test
public void noExternalIds() throws Exception {
db.accountExternalIds().delete(db.accountExternalIds().byAccount(user.getAccountId()));
ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
externalIdsUpdate.deleteAll(db, user.getAccountId());
reloadUser();
TestKey key = validKeyWithSecondUserId();
@ -234,11 +236,8 @@ public class GerritPublicKeyCheckerTest {
checker = checkerFactory.create().setStore(store).disableTrust();
assertProblems(
checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users");
db.accountExternalIds()
.insert(
Collections.singleton(
new AccountExternalId(user.getAccountId(), toExtIdKey(key.getPublicKey()))));
externalIdsUpdate.insert(
db, ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId()));
reloadUser();
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 {
Account.Id id = user.getAccountId();
List<AccountExternalId> newExtIds = new ArrayList<>(2);
newExtIds.add(new AccountExternalId(id, toExtIdKey(kr.getPublicKey())));
List<ExternalId> newExtIds = new ArrayList<>(2);
newExtIds.add(ExternalId.create(toExtIdKey(kr.getPublicKey()), id));
@SuppressWarnings("unchecked")
String userId = (String) Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null);
if (userId != null) {
String email = PushCertificateIdent.parse(userId).getEmailAddress();
assertThat(email).contains("@");
AccountExternalId mailto =
new AccountExternalId(id, new AccountExternalId.Key(SCHEME_MAILTO, email));
mailto.setEmailAddress(email);
newExtIds.add(mailto);
newExtIds.add(ExternalId.createEmail(id, email));
}
store.add(kr);
@ -410,7 +406,7 @@ public class GerritPublicKeyCheckerTest {
cb.setCommitter(ident);
assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
db.accountExternalIds().insert(newExtIds);
externalIdsUpdateFactory.create().insert(db, newExtIds);
accountCache.evict(user.getAccountId());
}
@ -434,12 +430,9 @@ public class GerritPublicKeyCheckerTest {
}
private void addExternalId(String scheme, String id, String email) throws Exception {
AccountExternalId extId =
new AccountExternalId(user.getAccountId(), new AccountExternalId.Key(scheme, id));
if (email != null) {
extId.setEmailAddress(email);
}
db.accountExternalIds().insert(Collections.singleton(extId));
externalIdsUpdateFactory
.create()
.insert(db, ExternalId.createWithEmail(scheme, id, user.getAccountId(), email));
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.Val;
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.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Provider;
import com.google.inject.servlet.RequestScoped;
@ -132,7 +132,7 @@ public abstract class CacheBasedWebSession implements WebSession {
}
@Override
public AccountExternalId.Key getLastLoginExternalId() {
public ExternalId.Key getLastLoginExternalId() {
return val != null ? val.getExternalId() : null;
}
@ -149,9 +149,9 @@ public abstract class CacheBasedWebSession implements WebSession {
}
@Override
public void login(final AuthResult res, final boolean rememberMe) {
final Account.Id id = res.getAccountId();
final AccountExternalId.Key identity = res.getExternalId();
public void login(AuthResult res, boolean rememberMe) {
Account.Id id = res.getAccountId();
ExternalId.Key identity = res.getExternalId();
if (val != null) {
manager.destroy(key);

View File

@ -42,14 +42,10 @@ public class GitOverHttpModule extends ServletModule {
Class<? extends Filter> authFilter;
if (authConfig.isTrustContainerAuth()) {
authFilter = ContainerAuthFilter.class;
} else if (authConfig.isGitBasicAuth()) {
if (authConfig.getAuthType() == OAUTH) {
authFilter = ProjectOAuthFilter.class;
} else {
authFilter = ProjectBasicAuthFilter.class;
}
} else if (authConfig.getAuthType() == OAUTH) {
authFilter = ProjectOAuthFilter.class;
} else {
authFilter = ProjectDigestFilter.class;
authFilter = ProjectBasicAuthFilter.class;
}
if (isHttpEnabled()) {

View File

@ -140,7 +140,7 @@ class ProjectBasicAuthFilter implements Filter {
GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
|| gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
if (passwordMatchesTheUserGeneratedOne(who, username, password)) {
if (who.checkPassword(password, username)) {
return succeedAuthentication(who);
}
}
@ -157,7 +157,7 @@ class ProjectBasicAuthFilter implements Filter {
setUserIdentified(whoAuthResult.getAccountId());
return true;
} catch (NoSuchUserException e) {
if (password.equals(who.getPassword(who.getUserName()))) {
if (who.checkPassword(password, who.getUserName())) {
return succeedAuthentication(who);
}
log.warn("Authentication failed for " + username, e);
@ -193,12 +193,6 @@ class ProjectBasicAuthFilter implements Filter {
ws.setAccessPathOk(AccessPath.REST_API, true);
}
private boolean passwordMatchesTheUserGeneratedOne(
AccountState who, String username, String password) {
String accountPassword = who.getPassword(username);
return accountPassword != null && password != null && accountPassword.equals(password);
}
private String encoding(HttpServletRequest req) {
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
}

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.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
public interface WebSession {
boolean isSignedIn();
@ -29,7 +29,7 @@ public interface WebSession {
boolean isValidXGerritAuth(String keyIn);
AccountExternalId.Key getLastLoginExternalId();
ExternalId.Key getLastLoginExternalId();
CurrentUser getUser();

View File

@ -30,7 +30,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.cache.Cache;
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.GerritServerConfig;
import com.google.inject.Inject;
@ -98,18 +98,18 @@ public class WebSessionManager {
}
}
Val createVal(final Key key, final Val val) {
final Account.Id who = val.getAccountId();
final boolean remember = val.isPersistentCookie();
final AccountExternalId.Key lastLogin = val.getExternalId();
Val createVal(Key key, Val val) {
Account.Id who = val.getAccountId();
boolean remember = val.isPersistentCookie();
ExternalId.Key lastLogin = val.getExternalId();
return createVal(key, who, remember, lastLogin, val.sessionId, val.auth);
}
Val createVal(
final Key key,
final Account.Id who,
final boolean remember,
final AccountExternalId.Key lastLogin,
Key key,
Account.Id who,
boolean remember,
ExternalId.Key lastLogin,
String sid,
String auth) {
// 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 long refreshCookieAt;
private transient boolean persistentCookie;
private transient AccountExternalId.Key externalId;
private transient ExternalId.Key externalId;
private transient long expiresAt;
private transient String sessionId;
private transient String auth;
Val(
final Account.Id accountId,
final long refreshCookieAt,
final boolean persistentCookie,
final AccountExternalId.Key externalId,
final long expiresAt,
final String sessionId,
final String auth) {
Account.Id accountId,
long refreshCookieAt,
boolean persistentCookie,
ExternalId.Key externalId,
long expiresAt,
String sessionId,
String auth) {
this.accountId = accountId;
this.refreshCookieAt = refreshCookieAt;
this.persistentCookie = persistentCookie;
@ -221,7 +221,7 @@ public class WebSessionManager {
return accountId;
}
AccountExternalId.Key getExternalId() {
ExternalId.Key getExternalId() {
return externalId;
}
@ -253,7 +253,7 @@ public class WebSessionManager {
if (externalId != null) {
writeVarInt32(out, 4);
writeString(out, externalId.get());
writeString(out, externalId.toString());
}
if (sessionId != null) {
@ -289,7 +289,7 @@ public class WebSessionManager {
persistentCookie = readVarInt32(in) != 0;
continue;
case 4:
externalId = new AccountExternalId.Key(readString(in));
externalId = ExternalId.Key.parse(readString(in));
continue;
case 5:
sessionId = readString(in);

View File

@ -14,7 +14,8 @@
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.extensions.registration.DynamicItem;
@ -23,13 +24,13 @@ import com.google.gerrit.httpd.LoginUrlToken;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.httpd.template.SiteHeaderFooter;
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.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest;
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.gwtexpui.server.CacheHeaders;
import com.google.gwtorm.server.OrmException;
@ -179,17 +180,16 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
return null;
}
private AuthResult auth(final AccountExternalId account) {
private AuthResult auth(Account.Id account) {
if (account != null) {
return new AuthResult(account.getAccountId(), null, false);
return new AuthResult(account, null, false);
}
return null;
}
private AuthResult byUserName(final String userName) {
try {
AccountExternalId.Key extKey = new AccountExternalId.Key(SCHEME_USERNAME, userName);
List<AccountState> accountStates = accountQuery.byExternalId(extKey.get());
List<AccountState> accountStates = accountQuery.byExternalId(SCHEME_USERNAME, userName);
if (accountStates.isEmpty()) {
getServletContext().log("No accounts with username " + userName + " found");
return null;
@ -198,7 +198,7 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
getServletContext().log("Multiple accounts with username " + userName + " found");
return null;
}
return auth(new AccountExternalId(accountStates.get(0).getAccount().getId(), extKey));
return auth(accountStates.get(0).getAccount().getId());
} catch (OrmException e) {
getServletContext().log("cannot query account index", e);
return null;
@ -231,9 +231,9 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
}
private AuthResult create() throws IOException {
String fakeId = AccountExternalId.SCHEME_UUID + UUID.randomUUID();
try {
return accountManager.authenticate(new AuthRequest(fakeId));
return accountManager.authenticate(
new AuthRequest(ExternalId.Key.create(SCHEME_UUID, UUID.randomUUID().toString())));
} catch (AccountException e) {
getServletContext().log("cannot create new account", e);
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.Strings.emptyToNull;
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.UTF_8;
@ -26,7 +26,7 @@ import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.RemoteUserUtil;
import com.google.gerrit.httpd.WebSession;
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.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.RPCServletUtils;
@ -127,8 +127,8 @@ class HttpAuthFilter implements Filter {
}
private static boolean correctUser(String user, WebSession session) {
AccountExternalId.Key id = session.getLastLoginExternalId();
return id != null && id.equals(new AccountExternalId.Key(SCHEME_GERRIT, user));
ExternalId.Key id = session.getLastLoginExternalId();
return id != null && id.equals(ExternalId.Key.create(SCHEME_GERRIT, user));
}
String getRemoteUser(HttpServletRequest req) {

View File

@ -14,7 +14,7 @@
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 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.LoginUrlToken;
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.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtorm.server.OrmException;
@ -39,6 +39,7 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
@ -127,7 +128,7 @@ class HttpLoginServlet extends HttpServlet {
try {
log.debug("Associating external identity \"{}\" to user \"{}\"", remoteExternalId, user);
updateRemoteExternalId(arsp, remoteExternalId);
} catch (AccountException | OrmException e) {
} catch (AccountException | OrmException | ConfigInvalidException e) {
log.error(
"Unable to associate external identity \""
+ remoteExternalId
@ -156,12 +157,10 @@ class HttpLoginServlet extends HttpServlet {
}
private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken)
throws AccountException, OrmException, IOException {
AccountExternalId remoteAuthExtId =
new AccountExternalId(
arsp.getAccountId(), new AccountExternalId.Key(SCHEME_EXTERNAL, remoteAuthToken));
throws AccountException, OrmException, IOException, ConfigInvalidException {
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) {

View File

@ -22,6 +22,7 @@ java_library(
"//lib/commons:codec",
"//lib/guice",
"//lib/guice:guice-servlet",
"//lib/jgit/org.eclipse.jgit:jgit",
"//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.AuthRequest;
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.gwtorm.server.OrmException;
import com.google.inject.Inject;
@ -44,6 +45,7 @@ import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -124,7 +126,7 @@ class OAuthSession {
private void authenticateAndRedirect(
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;
try {
String claimedIdentifier = user.getClaimedIdentity();
@ -190,7 +192,7 @@ class OAuthSession {
log.info("OAuth2: linking claimed identity to {}", claimedId.get().toString());
try {
accountManager.link(claimedId.get(), req);
} catch (OrmException e) {
} catch (OrmException | ConfigInvalidException e) {
log.error(
"Cannot link: "
+ user.getExternalId()
@ -210,7 +212,7 @@ class OAuthSession {
throws AccountException, IOException {
try {
accountManager.link(identifiedUser.get().getAccountId(), areq);
} catch (OrmException e) {
} catch (OrmException | ConfigInvalidException e) {
log.error(
"Cannot link: "
+ 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.AccountManager;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@ -43,6 +44,7 @@ import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -116,7 +118,8 @@ class OAuthSessionOverOpenID {
private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp)
throws IOException {
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;
try {
String claimedIdentifier = user.getClaimedIdentity();
@ -167,7 +170,7 @@ class OAuthSessionOverOpenID {
log.debug("Claimed account already exists: link to it.");
try {
accountManager.link(claimedId.get(), areq);
} catch (OrmException e) {
} catch (OrmException | ConfigInvalidException e) {
log.error(
"Cannot link: "
+ user.getExternalId()
@ -186,7 +189,7 @@ class OAuthSessionOverOpenID {
try {
log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(), accountId);
accountManager.link(accountId, areq);
} catch (OrmException e) {
} catch (OrmException | ConfigInvalidException e) {
log.error("Cannot link: " + user.getExternalId() + " to user identity: " + accountId);
rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
return;

View File

@ -26,6 +26,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.UrlEncoded;
import com.google.gerrit.server.account.AccountException;
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.config.AuthConfig;
import com.google.gerrit.server.config.ConfigUtil;
@ -314,7 +315,7 @@ class OpenIdServiceImpl {
}
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) {
areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
@ -369,7 +370,7 @@ class OpenIdServiceImpl {
// link between the two, so set one up if not present.
//
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.get().equals(actualId.get())) {
@ -388,7 +389,7 @@ class OpenIdServiceImpl {
+ " Delgate ID: "
+ actualId.get()
+ " is "
+ areq.getExternalId());
+ areq.getExternalIdKey());
cancelWithError(req, rsp, "Contact site administrator");
return;
}
@ -398,7 +399,8 @@ class OpenIdServiceImpl {
// was missing due to a bug in Gerrit. Link the claimed.
//
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.setEmailAddress(areq.getEmailAddress());
accountManager.link(actualId.get(), linkReq);
@ -434,7 +436,8 @@ class OpenIdServiceImpl {
webSession.get().login(arsp, remember);
if (arsp.isNew() && claimedIdentifier != null) {
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.setEmailAddress(areq.getEmailAddress());
accountManager.link(arsp.getAccountId(), linkReq);

View File

@ -14,115 +14,67 @@
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 com.google.gerrit.lifecycle.LifecycleManager;
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.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsBatchUpdate;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Injector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Collection;
import java.util.Locale;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.kohsuke.args4j.Option;
/** Converts the local username for all accounts to lower case */
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 TextProgressMonitor monitor = new TextProgressMonitor();
private List<AccountExternalId> todo;
private Injector dbInjector;
@Inject private SchemaFactory<ReviewDb> database;
@Inject private ExternalIdsBatchUpdate externalIdsBatchUpdate;
@Override
public int run() throws Exception {
if (threads <= 0) {
threads = 1;
}
dbInjector = createDbInjector(MULTI_USER);
Injector dbInjector = createDbInjector(MULTI_USER);
manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module()));
manager.start();
dbInjector.injectMembers(this);
try (ReviewDb db = database.open()) {
todo = db.accountExternalIds().all().toList();
synchronized (monitor) {
monitor.beginTask("Converting local usernames", todo.size());
}
}
Collection<ExternalId> todo = ExternalId.from(db.accountExternalIds().all().toList());
monitor.beginTask("Converting local usernames", todo.size());
final List<Worker> workers = new ArrayList<>(threads);
for (int tid = 0; tid < threads; tid++) {
Worker t = new Worker();
t.start();
workers.add(t);
}
for (Worker t : workers) {
t.join();
}
synchronized (monitor) {
monitor.endTask();
for (ExternalId extId : todo) {
convertLocalUserToLowerCase(extId);
monitor.update(1);
}
externalIdsBatchUpdate.commit(db, "Convert local usernames to lower case");
}
monitor.endTask();
manager.stop();
return 0;
}
private void convertLocalUserToLowerCase(final ReviewDb db, final AccountExternalId extId) {
if (extId.isScheme(AccountExternalId.SCHEME_GERRIT)) {
final String localUser = extId.getSchemeRest();
final String localUserLowerCase = localUser.toLowerCase(Locale.US);
private void convertLocalUserToLowerCase(ExternalId extId) {
if (extId.isScheme(SCHEME_GERRIT)) {
String localUser = extId.key().id();
String localUserLowerCase = localUser.toLowerCase(Locale.US);
if (!localUser.equals(localUserLowerCase)) {
final AccountExternalId.Key extIdKeyLowerCase =
new AccountExternalId.Key(AccountExternalId.SCHEME_GERRIT, localUserLowerCase);
final AccountExternalId extIdLowerCase =
new AccountExternalId(extId.getAccountId(), extIdKeyLowerCase);
try {
db.accountExternalIds().insert(Collections.singleton(extIdLowerCase));
db.accountExternalIds().delete(Collections.singleton(extId));
} catch (OrmException error) {
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();
ExternalId extIdLowerCase =
ExternalId.create(
SCHEME_GERRIT,
localUserLowerCase,
extId.accountId(),
extId.email(),
extId.password());
externalIdsBatchUpdate.replace(extId, extIdLowerCase);
}
}
}

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.InitStep;
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.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gwtorm.server.SchemaFactory;
@ -48,15 +48,20 @@ public class InitAdminUser implements InitStep {
private final ConsoleUI ui;
private final InitFlags flags;
private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
private final ExternalIdsOnInit externalIds;
private SchemaFactory<ReviewDb> dbFactory;
private AccountIndexCollection indexCollection;
@Inject
InitAdminUser(
InitFlags flags, ConsoleUI ui, VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory) {
InitFlags flags,
ConsoleUI ui,
VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
ExternalIdsOnInit externalIds) {
this.flags = flags;
this.ui = ui;
this.authorizedKeysFactory = authorizedKeysFactory;
this.externalIds = externalIds;
}
@Override
@ -90,24 +95,13 @@ public class InitAdminUser implements InitStep {
AccountSshKey sshKey = readSshKey(id);
String email = readEmail(sshKey);
List<AccountExternalId> extIds = new ArrayList<>(2);
AccountExternalId extUser =
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));
List<ExternalId> extIds = new ArrayList<>(2);
extIds.add(ExternalId.createUsername(username, id, httpPassword));
if (email != null) {
AccountExternalId extMailto =
new AccountExternalId(
id, new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email));
extMailto.setEmailAddress(email);
extIds.add(extMailto);
db.accountExternalIds().insert(Collections.singleton(extMailto));
extIds.add(ExternalId.createEmail(id, email));
}
externalIds.insert(db, "Add external IDs for initial admin user", extIds);
Account a = new Account(id, TimeUtil.nowTs());
a.setFullName(name);
@ -123,7 +117,7 @@ public class InitAdminUser implements InitStep {
if (sshKey != null) {
VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
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());

View File

@ -25,16 +25,16 @@ import java.sql.Timestamp;
/**
* Information about a single user.
*
* <p>A user may have multiple identities they can use to login to Gerrit (see {@link
* AccountExternalId}), but in such cases they always map back to a single Account entity.
* <p>A user may have multiple identities they can use to login to Gerrit (see ExternalId), but in
* 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
* as part of their key structure):
*
* <ul>
* <li>{@link AccountExternalId}: OpenID identities and email addresses known to be registered to
* this user. Multiple records can exist when the user has more than one public identity, such
* as a work and a personal email address.
* <li>ExternalId: OpenID identities and email addresses known to be registered to this user.
* Multiple records can exist when the user has more than one public identity, such as a work
* and a personal email address.
* <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.
* <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.gwtorm.client.Column;
import com.google.gwtorm.client.StringKey;
import java.util.Objects;
/** Association of an external account identifier to a local {@link Account}. */
public final class AccountExternalId {
@ -87,6 +88,8 @@ public final class AccountExternalId {
@Column(id = 3, notNull = false)
protected String emailAddress;
// Encoded version of the hashed and salted password, to be interpreted by the
// {@link HashedPassword} class.
@Column(id = 4, notNull = false)
protected String password;
@ -140,12 +143,12 @@ public final class AccountExternalId {
return null != scheme ? getExternalId().substring(scheme.length() + 1) : null;
}
public String getPassword() {
return password;
public void setPassword(String hashed) {
password = hashed;
}
public void setPassword(String p) {
password = p;
public String getPassword() {
return password;
}
public boolean isTrusted() {
@ -163,4 +166,21 @@ public final class AccountExternalId {
public void setCanDelete(final boolean 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} */
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} */
public static final String REFS_USERS = "refs/users/";

View File

@ -230,9 +230,11 @@ junit_tests(
"//lib:guava",
"//lib:guava-retrying",
"//lib:protobuf",
"//lib/bouncycastle:bcprov",
"//lib/dropwizard:dropwizard-core",
"//lib/guice:guice-assistedinject",
"//lib/prolog:runtime",
"//lib/commons:codec",
],
)

View File

@ -16,8 +16,8 @@ package com.google.gerrit.server;
import com.google.gerrit.common.Nullable;
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.ExternalId;
import com.google.gerrit.server.account.GroupMembership;
import com.google.inject.servlet.RequestScoped;
import java.util.function.Consumer;
@ -44,7 +44,7 @@ public abstract class CurrentUser {
private AccessPath accessPath = AccessPath.UNKNOWN;
private CapabilityControl capabilities;
private PropertyKey<AccountExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create();
private PropertyKey<ExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create();
protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) {
this.capabilityControlFactory = capabilityControlFactory;
@ -151,11 +151,11 @@ public abstract class CurrentUser {
*/
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);
}
public AccountExternalId.Key getLastLoginExternalIdKey() {
public ExternalId.Key getLastLoginExternalIdKey() {
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.collect.Sets;
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.mail.send.EmailSender;
import com.google.inject.Inject;
@ -53,8 +52,8 @@ public abstract class AbstractRealm implements Realm {
@Override
public boolean hasEmailAddress(IdentifiedUser user, String email) {
for (AccountExternalId ext : user.state().getExternalIds()) {
if (email != null && email.equalsIgnoreCase(ext.getEmailAddress())) {
for (ExternalId ext : user.state().getExternalIds()) {
if (email != null && email.equalsIgnoreCase(ext.email())) {
return true;
}
}
@ -63,11 +62,11 @@ public abstract class AbstractRealm implements Realm {
@Override
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());
for (AccountExternalId ext : ids) {
if (!Strings.isNullOrEmpty(ext.getEmailAddress())) {
emails.add(ext.getEmailAddress());
for (ExternalId ext : ids) {
if (!Strings.isNullOrEmpty(ext.email())) {
emails.add(ext.email());
}
}
return emails;

View File

@ -18,7 +18,6 @@ import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
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.server.cache.CacheModule;
import com.google.gerrit.server.query.account.InternalAccountQuery;
@ -94,12 +93,15 @@ public class AccountByEmailCacheImpl implements AccountByEmailCache {
for (Account a : db.accounts().byPreferredEmail(email)) {
r.add(a.getId());
}
for (AccountState accountState :
accountQueryProvider
.get()
.byExternalId(
(new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email)).get())) {
r.add(accountState.getAccount().getId());
for (AccountState accountState : accountQueryProvider.get().byEmailPrefix(email)) {
if (accountState
.getExternalIds()
.stream()
.filter(e -> email.equals(e.email()))
.findAny()
.isPresent()) {
r.add(accountState.getAccount().getId());
}
}
return ImmutableSet.copyOf(r);
}

View File

@ -14,13 +14,14 @@
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.LoadingCache;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
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.AccountGroupMember;
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.name.Named;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -138,9 +138,9 @@ public class AccountCacheImpl implements AccountCache {
private static AccountState missing(Account.Id accountId) {
Account account = new Account(accountId, TimeUtil.nowTs());
account.setActive(false);
Collection<AccountExternalId> ids = Collections.emptySet();
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> {
@ -184,8 +184,8 @@ public class AccountCacheImpl implements AccountCache {
return missing(who);
}
Collection<AccountExternalId> externalIds =
Collections.unmodifiableCollection(db.accountExternalIds().byAccount(who).toList());
Set<ExternalId> externalIds =
ExternalId.from(db.accountExternalIds().byAccount(who).toList());
Set<AccountGroup.UUID> internalGroups = new HashSet<>();
for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
@ -219,11 +219,8 @@ public class AccountCacheImpl implements AccountCache {
@Override
public Optional<Account.Id> load(String username) throws Exception {
AccountExternalId.Key key =
new AccountExternalId.Key( //
AccountExternalId.SCHEME_USERNAME, //
username);
AccountState accountState = accountQueryProvider.get().oneByExternalId(key.get());
AccountState accountState =
accountQueryProvider.get().oneByExternalId(SCHEME_USERNAME, username);
return Optional.ofNullable(accountState).map(s -> s.getAccount().getId());
}
}

View File

@ -14,6 +14,8 @@
package com.google.gerrit.server.account;
import static java.util.stream.Collectors.toSet;
import com.google.common.base.Strings;
import com.google.gerrit.audit.AuditService;
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.extensions.client.AccountFieldName;
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.AccountGroupMember;
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.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -60,6 +60,7 @@ public class AccountManager {
private final AtomicBoolean awaitsFirstAccountCheck;
private final AuditService auditService;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
@Inject
AccountManager(
@ -71,7 +72,8 @@ public class AccountManager {
ChangeUserName.Factory changeUserNameFactory,
ProjectCache projectCache,
AuditService auditService,
Provider<InternalAccountQuery> accountQueryProvider) {
Provider<InternalAccountQuery> accountQueryProvider,
ExternalIdsUpdate.Server externalIdsUpdateFactory) {
this.schema = schema;
this.byIdCache = byIdCache;
this.byEmailCache = byEmailCache;
@ -82,6 +84,7 @@ public class AccountManager {
this.awaitsFirstAccountCheck = new AtomicBoolean(true);
this.auditService = auditService;
this.accountQueryProvider = accountQueryProvider;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
}
/** @return user identified by this external identity string */
@ -108,8 +111,7 @@ public class AccountManager {
who = realm.authenticate(who);
try {
try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who);
AccountExternalId id = getAccountExternalId(key);
ExternalId id = findExternalId(who.getExternalIdKey());
if (id == null) {
// New account, automatically create and return.
//
@ -117,25 +119,25 @@ public class AccountManager {
}
// Account exists
Account act = byIdCache.get(id.getAccountId()).getAccount();
Account act = byIdCache.get(id.accountId()).getAccount();
if (!act.isActive()) {
throw new AccountException("Authentication error, account inactive");
}
// return the identity to the caller.
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);
}
}
private AccountExternalId getAccountExternalId(AccountExternalId.Key key) throws OrmException {
AccountState accountState = accountQueryProvider.get().oneByExternalId(key.get());
private ExternalId findExternalId(ExternalId.Key key) throws OrmException {
AccountState accountState = accountQueryProvider.get().oneByExternalId(key);
if (accountState != null) {
for (AccountExternalId extId : accountState.getExternalIds()) {
if (extId.getKey().equals(key)) {
for (ExternalId extId : accountState.getExternalIds()) {
if (extId.key().equals(key)) {
return extId;
}
}
@ -143,24 +145,28 @@ public class AccountManager {
return null;
}
private void update(ReviewDb db, AuthRequest who, AccountExternalId extId)
throws OrmException, IOException {
IdentifiedUser user = userFactory.create(extId.getAccountId());
private void update(ReviewDb db, AuthRequest who, ExternalId extId)
throws OrmException, IOException, ConfigInvalidException {
IdentifiedUser user = userFactory.create(extId.accountId());
Account toUpdate = null;
// If the email address was modified by the authentication provider,
// update our records to match the changed email.
//
String newEmail = who.getEmailAddress();
String oldEmail = extId.getEmailAddress();
String oldEmail = extId.email();
if (newEmail != null && !newEmail.equals(oldEmail)) {
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
toUpdate = load(toUpdate, user.getAccountId(), db);
toUpdate.setPreferredEmail(newEmail);
}
extId.setEmailAddress(newEmail);
db.accountExternalIds().update(Collections.singleton(extId));
externalIdsUpdateFactory
.create()
.replace(
db,
extId,
ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password()));
}
if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
@ -206,14 +212,14 @@ public class AccountManager {
}
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 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.setPreferredEmail(extId.getEmailAddress());
account.setPreferredEmail(extId.email());
boolean isFirstAccount =
awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty();
@ -221,18 +227,19 @@ public class AccountManager {
try {
db.accounts().upsert(Collections.singleton(account));
AccountExternalId existingExtId = db.accountExternalIds().get(extId.getKey());
if (existingExtId != null && !existingExtId.getAccountId().equals(extId.getAccountId())) {
ExternalId existingExtId =
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
db.accounts().delete(Collections.singleton(account));
throw new AccountException(
"Cannot assign external ID \""
+ extId.getExternalId()
+ extId.key().get()
+ "\" to account "
+ newId
+ "; external ID already in use.");
}
db.accountExternalIds().upsert(Collections.singleton(extId));
externalIdsUpdateFactory.create().upsert(db, extId);
} finally {
// 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
@ -291,7 +298,7 @@ public class AccountManager {
byEmailCache.evict(account.getPreferredEmail());
byIdCache.evict(account.getId());
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(
ReviewDb db,
Account account,
AccountExternalId extId,
ExternalId extId,
String errorMessage,
Exception e,
boolean logException)
throws AccountUserNameException, OrmException {
throws AccountUserNameException, OrmException, IOException, ConfigInvalidException {
if (logException) {
log.error(errorMessage, e);
} else {
@ -333,16 +340,11 @@ public class AccountManager {
// this is why the best we can do here is to fail early and cleanup
// the database
db.accounts().delete(Collections.singleton(account));
db.accountExternalIds().delete(Collections.singleton(extId));
externalIdsUpdateFactory.create().delete(db, extId);
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.
*
@ -353,19 +355,19 @@ public class AccountManager {
* this time.
*/
public AuthResult link(Account.Id to, AuthRequest who)
throws AccountException, OrmException, IOException {
throws AccountException, OrmException, IOException, ConfigInvalidException {
try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who);
AccountExternalId extId = getAccountExternalId(key);
ExternalId extId = findExternalId(who.getExternalIdKey());
if (extId != null) {
if (!extId.getAccountId().equals(to)) {
if (!extId.accountId().equals(to)) {
throw new AccountException("Identity in use by another account");
}
update(db, who, extId);
} else {
extId = createId(to, who);
extId.setEmailAddress(who.getEmailAddress());
db.accountExternalIds().insert(Collections.singleton(extId));
externalIdsUpdateFactory
.create()
.insert(
db, ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
if (who.getEmailAddress() != null) {
Account a = db.accounts().get(to);
@ -381,7 +383,7 @@ public class AccountManager {
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.
*/
public AuthResult updateLink(Account.Id to, AuthRequest who)
throws OrmException, AccountException, IOException {
throws OrmException, AccountException, IOException, ConfigInvalidException {
try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who);
List<AccountExternalId.Key> filteredKeysByScheme =
filterKeysByScheme(key.getScheme(), db.accountExternalIds().byAccount(to));
if (!filteredKeysByScheme.isEmpty()
&& (filteredKeysByScheme.size() > 1 || !filteredKeysByScheme.contains(key))) {
db.accountExternalIds().deleteKeys(filteredKeysByScheme);
Collection<ExternalId> filteredExtIdsByScheme =
ExternalId.from(db.accountExternalIds().byAccount(to).toList())
.stream()
.filter(e -> e.isScheme(who.getExternalIdKey().scheme()))
.collect(toSet());
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);
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.
*
@ -434,15 +433,15 @@ public class AccountManager {
* at this time.
*/
public AuthResult unlink(Account.Id from, AuthRequest who)
throws AccountException, OrmException, IOException {
throws AccountException, OrmException, IOException, ConfigInvalidException {
try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who);
AccountExternalId extId = getAccountExternalId(key);
ExternalId extId = findExternalId(who.getExternalIdKey());
if (extId != null) {
if (!extId.getAccountId().equals(from)) {
throw new AccountException("Identity '" + key.get() + "' in use by another account");
if (!extId.accountId().equals(from)) {
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) {
Account a = db.accounts().get(from);
@ -456,14 +455,10 @@ public class AccountManager {
}
} 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;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gerrit.common.Nullable;
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.CurrentUser.PropertyKey;
import com.google.gerrit.server.IdentifiedUser;
@ -32,21 +32,26 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.codec.DecoderException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccountState {
private static final Logger logger = LoggerFactory.getLogger(AccountState.class);
public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
a -> a.getAccount().getId();
private final Account account;
private final Set<AccountGroup.UUID> internalGroups;
private final Collection<AccountExternalId> externalIds;
private final Collection<ExternalId> externalIds;
private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
public AccountState(
Account account,
Set<AccountGroup.UUID> actualGroups,
Collection<AccountExternalId> externalIds,
Collection<ExternalId> externalIds,
Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
this.account = account;
this.internalGroups = actualGroups;
@ -63,25 +68,38 @@ public class AccountState {
/**
* Get the username, if one has been declared for this user.
*
* <p>The username is the {@link AccountExternalId} using the scheme {@link
* AccountExternalId#SCHEME_USERNAME}.
* <p>The username is the {@link ExternalId} using the scheme {@link ExternalId#SCHEME_USERNAME}.
*/
public String getUserName() {
return account.getUserName();
}
/** @return the password matching the requested username; or null. */
public String getPassword(String username) {
for (AccountExternalId id : getExternalIds()) {
if (id.isScheme(AccountExternalId.SCHEME_USERNAME) && username.equals(id.getSchemeRest())) {
return id.getPassword();
public boolean checkPassword(String password, String username) {
if (password == null) {
return false;
}
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. */
public Collection<AccountExternalId> getExternalIds() {
public Collection<ExternalId> getExternalIds() {
return externalIds;
}
@ -95,20 +113,20 @@ public class AccountState {
return internalGroups;
}
public static String getUserName(Collection<AccountExternalId> ids) {
for (AccountExternalId id : ids) {
if (id.isScheme(SCHEME_USERNAME)) {
return id.getSchemeRest();
public static String getUserName(Collection<ExternalId> ids) {
for (ExternalId extId : ids) {
if (extId.isScheme(SCHEME_USERNAME)) {
return extId.key().id();
}
}
return null;
}
public static Set<String> getEmails(Collection<AccountExternalId> ids) {
public static Set<String> getEmails(Collection<ExternalId> ids) {
Set<String> emails = new HashSet<>();
for (AccountExternalId id : ids) {
if (id.isScheme(SCHEME_MAILTO)) {
emails.add(id.getSchemeRest());
for (ExternalId extId : ids) {
if (extId.isScheme(SCHEME_MAILTO)) {
emails.add(extId.key().id());
}
}
return emails;

View File

@ -14,11 +14,9 @@
package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL;
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
/**
* Information for {@link AccountManager#authenticate(AuthRequest)}.
@ -30,17 +28,15 @@ import com.google.gerrit.reviewdb.client.AccountExternalId;
*/
public class AuthRequest {
/** Create a request for a local username, such as from LDAP. */
public static AuthRequest forUser(final String username) {
final AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_GERRIT, username);
final AuthRequest r = new AuthRequest(i.get());
public static AuthRequest forUser(String username) {
AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_GERRIT, username));
r.setUserName(username);
return r;
}
/** Create a request for an external username. */
public static AuthRequest forExternalUser(String username) {
AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_EXTERNAL, username);
AuthRequest r = new AuthRequest(i.get());
AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, username));
r.setUserName(username);
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
* account.
*/
public static AuthRequest forEmail(final String email) {
final AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_MAILTO, email);
final AuthRequest r = new AuthRequest(i.get());
public static AuthRequest forEmail(String email) {
AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_MAILTO, email));
r.setEmailAddress(email);
return r;
}
private String externalId;
private ExternalId.Key externalId;
private String password;
private String displayName;
private String emailAddress;
@ -67,29 +62,24 @@ public class AuthRequest {
private String authPlugin;
private String authProvider;
public AuthRequest(final String externalId) {
public AuthRequest(ExternalId.Key externalId) {
this.externalId = externalId;
}
public String getExternalId() {
public ExternalId.Key getExternalIdKey() {
return externalId;
}
public boolean isScheme(final String scheme) {
return getExternalId().startsWith(scheme);
}
public String getLocalUser() {
if (isScheme(SCHEME_GERRIT)) {
return getExternalId().substring(SCHEME_GERRIT.length());
if (externalId.isScheme(SCHEME_GERRIT)) {
return externalId.id();
}
return null;
}
public void setLocalUser(final String localUser) {
if (isScheme(SCHEME_GERRIT)) {
final AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_GERRIT, localUser);
externalId = key.get();
public void setLocalUser(String localUser) {
if (externalId.isScheme(SCHEME_GERRIT)) {
externalId = ExternalId.Key.create(SCHEME_GERRIT, localUser);
}
}

View File

@ -15,16 +15,14 @@
package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
/** Result from {@link AccountManager#authenticate(AuthRequest)}. */
public class AuthResult {
private final Account.Id accountId;
private final AccountExternalId.Key externalId;
private final ExternalId.Key externalId;
private final boolean isNew;
public AuthResult(
final Account.Id accountId, final AccountExternalId.Key externalId, final boolean isNew) {
public AuthResult(Account.Id accountId, ExternalId.Key externalId, boolean isNew) {
this.accountId = accountId;
this.externalId = externalId;
this.isNew = isNew;
@ -36,7 +34,7 @@ public class AuthResult {
}
/** External identity used to authenticate the user. */
public AccountExternalId.Key getExternalId() {
public ExternalId.Key getExternalId() {
return externalId;
}

View File

@ -14,12 +14,12 @@
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.errors.NameAlreadyUsedException;
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.server.IdentifiedUser;
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.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
/** Operation to change the username of an account. */
public class ChangeUserName implements Callable<VoidResult> {
@ -48,6 +47,7 @@ public class ChangeUserName implements Callable<VoidResult> {
private final AccountCache accountCache;
private final SshKeyCache sshKeyCache;
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
private final ReviewDb db;
private final IdentifiedUser user;
@ -55,14 +55,15 @@ public class ChangeUserName implements Callable<VoidResult> {
@Inject
ChangeUserName(
final AccountCache accountCache,
final SshKeyCache sshKeyCache,
@Assisted final ReviewDb db,
@Assisted final IdentifiedUser user,
@Nullable @Assisted final String newUsername) {
AccountCache accountCache,
SshKeyCache sshKeyCache,
ExternalIdsUpdate.Server externalIdsUpdateFactory,
@Assisted ReviewDb db,
@Assisted IdentifiedUser user,
@Nullable @Assisted String newUsername) {
this.accountCache = accountCache;
this.sshKeyCache = sshKeyCache;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.db = db;
this.user = user;
this.newUsername = newUsername;
@ -70,33 +71,38 @@ public class ChangeUserName implements Callable<VoidResult> {
@Override
public VoidResult call()
throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException {
final Collection<AccountExternalId> old = old();
throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException,
ConfigInvalidException {
Collection<ExternalId> old =
ExternalId.from(db.accountExternalIds().byAccount(user.getAccountId()).toList())
.stream()
.filter(e -> e.isScheme(SCHEME_USERNAME))
.collect(toSet());
if (!old.isEmpty()) {
throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
}
ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
if (newUsername != null && !newUsername.isEmpty()) {
if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
throw new InvalidUserNameException();
}
final AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_USERNAME, newUsername);
ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, newUsername);
try {
final AccountExternalId id = new AccountExternalId(user.getAccountId(), key);
for (AccountExternalId i : old) {
if (i.getPassword() != null) {
id.setPassword(i.getPassword());
String password = null;
for (ExternalId i : old) {
if (i.password() != null) {
password = i.password();
}
}
db.accountExternalIds().insert(Collections.singleton(id));
externalIdsUpdate.insert(db, ExternalId.create(key, user.getAccountId(), null, password));
} catch (OrmDuplicateKeyException dupeErr) {
// If we are using this identity, don't report the exception.
//
AccountExternalId other = db.accountExternalIds().get(key);
if (other != null && other.getAccountId().equals(user.getAccountId())) {
ExternalId other =
ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey()));
if (other != null && other.accountId().equals(user.getAccountId())) {
return VoidResult.INSTANCE;
}
@ -108,10 +114,10 @@ public class ChangeUserName implements Callable<VoidResult> {
// If we have any older user names, remove them.
//
db.accountExternalIds().delete(old);
for (AccountExternalId i : old) {
sshKeyCache.evict(i.getSchemeRest());
accountCache.evictByUsername(i.getSchemeRest());
externalIdsUpdate.delete(db, old);
for (ExternalId extId : old) {
sshKeyCache.evict(extId.key().id());
accountCache.evictByUsername(extId.key().id());
}
accountCache.evict(user.getAccountId());
@ -119,14 +125,4 @@ public class ChangeUserName implements Callable<VoidResult> {
sshKeyCache.evict(newUsername);
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;
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.common.TimeUtil;
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.UnprocessableEntityException;
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.AccountGroupMember;
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 DynamicSet<AccountExternalIdCreator> externalIdCreators;
private final AuditService auditService;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
private final String username;
@Inject
@ -85,6 +87,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
AccountLoader.Factory infoLoader,
DynamicSet<AccountExternalIdCreator> externalIdCreators,
AuditService auditService,
ExternalIdsUpdate.User externalIdsUpdateFactory,
@Assisted String username) {
this.db = db;
this.currentUser = currentUser;
@ -97,6 +100,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
this.infoLoader = infoLoader;
this.externalIdCreators = externalIdCreators;
this.auditService = auditService;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.username = username;
}
@ -120,19 +124,14 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
Account.Id id = new Account.Id(db.nextAccountId());
AccountExternalId extUser =
new AccountExternalId(
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
if (input.httpPassword != null) {
extUser.setPassword(input.httpPassword);
}
if (db.accountExternalIds().get(extUser.getKey()) != null) {
ExternalId extUser = ExternalId.createUsername(username, id, input.httpPassword);
if (db.accountExternalIds().get(extUser.key().asAccountExternalIdKey()) != null) {
throw new ResourceConflictException("username '" + username + "' already exists");
}
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");
}
if (!OutgoingEmailValidator.isValid(input.email)) {
@ -140,27 +139,26 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
}
}
List<AccountExternalId> externalIds = new ArrayList<>();
externalIds.add(extUser);
List<ExternalId> extIds = new ArrayList<>();
extIds.add(extUser);
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 {
db.accountExternalIds().insert(externalIds);
externalIdsUpdate.insert(db, extIds);
} catch (OrmDuplicateKeyException duplicateKey) {
throw new ResourceConflictException("username '" + username + "' already exists");
}
if (input.email != null) {
AccountExternalId extMailto = new AccountExternalId(id, getEmailKey(input.email));
extMailto.setEmailAddress(input.email);
try {
db.accountExternalIds().insert(Collections.singleton(extMailto));
externalIdsUpdate.insert(db, ExternalId.createEmail(id, input.email));
} catch (OrmDuplicateKeyException duplicateKey) {
try {
db.accountExternalIds().delete(Collections.singleton(extUser));
} catch (OrmException cleanupError) {
externalIdsUpdate.delete(db, extUser);
} catch (IOException | ConfigInvalidException | OrmException cleanupError) {
// Ignored
}
throw new UnprocessableEntityException("email '" + input.email + "' already exists");
@ -208,8 +206,4 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
}
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.assistedinject.Assisted;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -77,7 +78,7 @@ public class CreateEmail implements RestModifyView<AccountResource, EmailInput>
public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input)
throws AuthException, BadRequestException, ResourceConflictException,
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
IOException {
IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
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)
throws AuthException, BadRequestException, ResourceConflictException,
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
IOException {
IOException, ConfigInvalidException {
if (input.email != null && !email.equals(input.email)) {
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.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@ -34,6 +33,7 @@ import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
@ -59,7 +59,7 @@ public class DeleteEmail implements RestModifyView<AccountResource.Email, Input>
@Override
public Response<?> apply(AccountResource.Email rsrc, Input input)
throws AuthException, ResourceNotFoundException, ResourceConflictException,
MethodNotAllowedException, OrmException, IOException {
MethodNotAllowedException, OrmException, IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
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)
throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException,
OrmException, IOException {
OrmException, IOException, ConfigInvalidException {
if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
throw new MethodNotAllowedException("realm does not allow deleting emails");
}
Set<AccountExternalId> extIds =
Set<ExternalId> extIds =
dbProvider
.get()
.accountExternalIds()
.byAccount(user.getAccountId())
.toList()
.stream()
.filter(e -> email.equals(e.getEmailAddress()))
.map(ExternalId::from)
.filter(e -> email.equals(e.email()))
.collect(toSet());
if (extIds.isEmpty()) {
throw new ResourceNotFoundException(email);
}
try {
for (AccountExternalId extId : extIds) {
AuthRequest authRequest = new AuthRequest(extId.getKey().get());
for (ExternalId extId : extIds) {
AuthRequest authRequest = new AuthRequest(extId.key());
authRequest.setEmailAddress(email);
accountManager.unlink(user.getAccountId(), authRequest);
}

View File

@ -14,7 +14,7 @@
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.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.UnprocessableEntityException;
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.server.CurrentUser;
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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> {
private final Provider<ReviewDb> db;
private final AccountByEmailCache accountByEmailCache;
private final AccountCache accountCache;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
@Inject
DeleteExternalIds(
Provider<ReviewDb> db,
AccountByEmailCache accountByEmailCache,
AccountCache accountCache,
ExternalIdsUpdate.User externalIdsUpdateFactory,
Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider) {
this.db = db;
this.accountByEmailCache = accountByEmailCache;
this.accountCache = accountCache;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.self = self;
this.dbProvider = dbProvider;
}
@Override
public Response<?> apply(AccountResource resource, List<String> externalIds)
throws RestApiException, IOException, OrmException {
throws RestApiException, IOException, OrmException, ConfigInvalidException {
if (self.get() != resource.getUser()) {
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();
Map<AccountExternalId.Key, AccountExternalId> externalIdMap =
db.get()
Map<ExternalId.Key, ExternalId> externalIdMap =
dbProvider
.get()
.accountExternalIds()
.byAccount(resource.getUser().getAccountId())
.toList()
.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<>();
AccountExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
List<ExternalId> toDelete = new ArrayList<>();
ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
for (String externalIdStr : externalIds) {
AccountExternalId id = externalIdMap.get(new AccountExternalId.Key(externalIdStr));
ExternalId id = externalIdMap.get(ExternalId.Key.parse(externalIdStr));
if (id == null) {
throw new UnprocessableEntityException(
@ -90,7 +90,7 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
}
if ((!id.isScheme(SCHEME_USERNAME))
&& ((last == null) || (!last.get().equals(id.getExternalId())))) {
&& ((last == null) || (!last.get().equals(id.key().get())))) {
toDelete.add(id);
} else {
throw new ResourceConflictException(
@ -99,10 +99,10 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
}
if (!toDelete.isEmpty()) {
dbProvider.get().accountExternalIds().delete(toDelete);
externalIdsUpdateFactory.create().delete(dbProvider.get(), toDelete);
accountCache.evict(accountId);
for (AccountExternalId e : toDelete) {
accountByEmailCache.evict(e.getEmailAddress());
for (ExternalId e : toDelete) {
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;
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.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.RestApiException;
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.server.CurrentUser;
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");
}
Collection<AccountExternalId> ids =
db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList();
Collection<ExternalId> ids =
ExternalId.from(
db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList());
if (ids.isEmpty()) {
return ImmutableList.of();
}
List<AccountExternalIdInfo> result = Lists.newArrayListWithCapacity(ids.size());
for (AccountExternalId id : ids) {
for (ExternalId id : ids) {
AccountExternalIdInfo info = new AccountExternalIdInfo();
info.identity = id.getExternalId();
info.emailAddress = id.getEmailAddress();
info.identity = id.key().get();
info.emailAddress = id.email();
info.trusted = toBoolean(authConfig.isIdentityTrustable(Collections.singleton(id)));
// The identity can be deleted only if its not the one used to
// establish this web session, and if only if an identity was
// actually used to establish this web session.
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));
}
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.registration.DynamicItem;
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.avatar.AvatarProvider;
import com.google.inject.AbstractModule;
@ -74,7 +73,7 @@ public class InternalAccountDirectory extends AccountDirectory {
private void fill(
AccountInfo info,
Account account,
@Nullable Collection<AccountExternalId> externalIds,
@Nullable Collection<ExternalId> externalIds,
Set<FillOptions> options) {
if (options.contains(FillOptions.ID)) {
info._accountId = account.getId().get();
@ -124,8 +123,7 @@ public class InternalAccountDirectory extends AccountDirectory {
}
}
public List<String> getSecondaryEmails(
Account account, Collection<AccountExternalId> externalIds) {
public List<String> getSecondaryEmails(Account account, Collection<ExternalId> externalIds) {
List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds));
if (account.getPreferredEmail() != null) {
emails.remove(account.getPreferredEmail());

View File

@ -56,7 +56,6 @@ public class Module extends RestApiModule {
put(EMAIL_KIND).to(PutEmail.class);
delete(EMAIL_KIND).to(DeleteEmail.class);
put(EMAIL_KIND, "preferred").to(PutPreferred.class);
get(ACCOUNT_KIND, "password.http").to(GetHttpPassword.class);
put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);

View File

@ -14,7 +14,7 @@
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.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.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@ -30,14 +29,12 @@ import com.google.gerrit.server.account.PutHttpPassword.Input;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collections;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
public static class Input {
public String httpPassword;
@ -58,19 +55,24 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final AccountCache accountCache;
private final ExternalIdsUpdate.User externalIdsUpdate;
@Inject
PutHttpPassword(
Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, AccountCache accountCache) {
Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider,
AccountCache accountCache,
ExternalIdsUpdate.User externalIdsUpdate) {
this.self = self;
this.dbProvider = dbProvider;
this.accountCache = accountCache;
this.externalIdsUpdate = externalIdsUpdate;
}
@Override
public Response<String> apply(AccountResource rsrc, Input input)
throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
IOException {
IOException, ConfigInvalidException {
if (input == null) {
input = new Input();
}
@ -100,21 +102,26 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
}
public Response<String> apply(IdentifiedUser user, String newPassword)
throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException {
throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException,
ConfigInvalidException {
if (user.getUserName() == null) {
throw new ResourceConflictException("username must be set");
}
AccountExternalId id =
dbProvider
.get()
.accountExternalIds()
.get(new AccountExternalId.Key(SCHEME_USERNAME, user.getUserName()));
if (id == null) {
ExternalId extId =
ExternalId.from(
dbProvider
.get()
.accountExternalIds()
.get(
ExternalId.Key.create(SCHEME_USERNAME, user.getUserName())
.asAccountExternalIdKey()));
if (extId == null) {
throw new ResourceNotFoundException();
}
id.setPassword(newPassword);
dbProvider.get().accountExternalIds().update(Collections.singleton(id));
ExternalId newExtId =
ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword);
externalIdsUpdate.create().upsert(dbProvider.get(), newExtId);
accountCache.evict(user.getAccountId());
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.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PutUsername implements RestModifyView<AccountResource, Input> {
@ -57,7 +58,7 @@ public class PutUsername implements RestModifyView<AccountResource, Input> {
@Override
public String apply(AccountResource rsrc, Input input)
throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
ResourceConflictException, OrmException, IOException {
ResourceConflictException, OrmException, IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
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);
try {
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);
}
}
@ -382,7 +382,7 @@ public class AccountApiImpl implements AccountApi {
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email);
try {
deleteEmail.apply(rsrc, null);
} catch (OrmException | IOException e) {
} catch (OrmException | IOException | ConfigInvalidException 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 {
try {
deleteExternalIds.apply(account, externalIds);
} catch (IOException | OrmException e) {
} catch (IOException | OrmException | ConfigInvalidException e) {
throw new RestApiException("Cannot delete external IDs", e);
}
}

View File

@ -15,7 +15,7 @@
package com.google.gerrit.server.api.accounts;
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;
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}.
* @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;
import com.google.gerrit.common.Nullable;
import java.util.Objects;
/** Defines an abstract request for user authentication to Gerrit. */
public abstract class AuthRequest {
@ -46,10 +45,4 @@ public abstract class AuthRequest {
public final String getPassword() {
return password;
}
public void checkPassword(String pwd) throws AuthException {
if (!Objects.equals(getPassword(), pwd)) {
throw new InvalidCredentialsException();
}
}
}

View File

@ -38,6 +38,7 @@ public class InternalAuthBackend implements AuthBackend {
return "gerrit";
}
// TODO(gerritcodereview-team): This function has no coverage.
@Override
public AuthUser authenticate(AuthRequest req)
throws MissingCredentialsException, InvalidCredentialsException, UnknownUserException,
@ -63,7 +64,9 @@ public class InternalAuthBackend implements AuthBackend {
+ ": account inactive or not provisioned in Gerrit");
}
req.checkPassword(who.getPassword(username));
if (!who.checkPassword(req.getPassword(), username)) {
throw new InvalidCredentialsException();
}
return new AuthUser(AuthUser.UUID.create(username), username);
}
}

View File

@ -14,6 +14,7 @@
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.auth.ldap.Helper.LDAP_UUID;
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.GroupReference;
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.server.CurrentUser;
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.GroupMembership;
import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
@ -180,10 +181,10 @@ public class LdapGroupBackend implements GroupBackend {
return new LdapGroupMembership(membershipCache, projectCache, id);
}
private static String findId(final Collection<AccountExternalId> ids) {
for (final AccountExternalId i : ids) {
if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
return i.getSchemeRest();
private static String findId(Collection<ExternalId> extIds) {
for (ExternalId extId : extIds) {
if (extId.isScheme(SCHEME_GERRIT)) {
return extId.key().id();
}
}
return null;

View File

@ -14,7 +14,7 @@
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.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.AuthType;
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.server.ReviewDb;
import com.google.gerrit.server.account.AbstractRealm;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AuthRequest;
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.auth.AuthenticationUnavailableException;
import com.google.gerrit.server.config.AuthConfig;
@ -329,8 +329,12 @@ class LdapRealm extends AbstractRealm {
public Optional<Account.Id> load(String username) throws Exception {
try (ReviewDb db = schema.open()) {
return Optional.ofNullable(
db.accountExternalIds().get(new AccountExternalId.Key(SCHEME_GERRIT, username)))
.map(AccountExternalId::getAccountId);
ExternalId.from(
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;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.account.ExternalId;
public class OpenIdProviderPattern {
public static OpenIdProviderPattern create(String pattern) {
@ -33,8 +33,8 @@ public class OpenIdProviderPattern {
return regex ? id.matches(pattern) : id.startsWith(pattern);
}
public boolean matches(AccountExternalId id) {
return matches(id.getExternalId());
public boolean matches(ExternalId extId) {
return matches(extId.key().get());
}
@Override

View File

@ -14,9 +14,13 @@
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.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.gwtjsonrpc.server.SignedToken;
import com.google.gwtjsonrpc.server.XsrfException;
@ -44,7 +48,6 @@ public class AuthConfig {
private final boolean trustContainerAuth;
private final boolean enableRunAs;
private final boolean userNameToLowerCase;
private final boolean gitBasicAuth;
private final boolean useContributorAgreements;
private final String loginUrl;
private final String loginText;
@ -88,7 +91,6 @@ public class AuthConfig {
cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
gitBasicAuthPolicy = getBasicAuthPolicy(cfg);
useContributorAgreements = cfg.getBoolean("auth", "contributoragreements", false);
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
@ -223,11 +225,6 @@ public class AuthConfig {
return userNameToLowerCase;
}
/** Whether git-over-http should use Gerrit basic authentication scheme. */
public boolean isGitBasicAuth() {
return gitBasicAuth;
}
public GitBasicAuthPolicy getGitBasicAuthPolicy() {
return gitBasicAuthPolicy;
}
@ -237,7 +234,7 @@ public class AuthConfig {
return useContributorAgreements;
}
public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
public boolean isIdentityTrustable(Collection<ExternalId> ids) {
switch (getAuthType()) {
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
case HTTP:
@ -258,7 +255,7 @@ public class AuthConfig {
case OPENID:
// All identities must be trusted in order to trust the account.
//
for (final AccountExternalId e : ids) {
for (ExternalId e : ids) {
if (!isTrusted(e)) {
return false;
}
@ -272,8 +269,8 @@ public class AuthConfig {
}
}
private boolean isTrusted(final AccountExternalId id) {
if (id.isScheme(AccountExternalId.SCHEME_MAILTO)) {
private boolean isTrusted(ExternalId id) {
if (id.isScheme(SCHEME_MAILTO)) {
// mailto identities are created by sending a unique validation
// token to the address and asking them to come back to the site
// with that token.
@ -281,20 +278,20 @@ public class AuthConfig {
return true;
}
if (id.isScheme(AccountExternalId.SCHEME_UUID)) {
if (id.isScheme(SCHEME_UUID)) {
// UUID identities are absolutely meaningless and cannot be
// constructed through any normal login process we use.
//
return true;
}
if (id.isScheme(AccountExternalId.SCHEME_USERNAME)) {
if (id.isScheme(SCHEME_USERNAME)) {
// We can trust their username, its local to our server only.
//
return true;
}
for (final OpenIdProviderPattern p : trustedOpenIDs) {
for (OpenIdProviderPattern p : trustedOpenIDs) {
if (p.matches(id)) {
return true;
}

View File

@ -30,6 +30,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
@ -54,7 +55,7 @@ public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
@Override
public Response<?> apply(ConfigResource rsrc, Input input)
throws AuthException, UnprocessableEntityException, AccountException, OrmException,
IOException {
IOException, ConfigInvalidException {
CurrentUser user = self.get();
if (!user.isIdentifiedUser()) {
throw new AuthException("Authentication required");

View File

@ -156,7 +156,6 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
info.useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
info.switchAccountUrl = cfg.getSwitchAccountUrl();
info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
info.gitBasicAuthPolicy = cfg.getGitBasicAuthPolicy();
if (info.useContributorAgreements != null) {

View File

@ -110,7 +110,8 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
String name = ref.getName();
Change.Id changeId;
Account.Id accountId;
if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) {
if (name.startsWith(REFS_CACHE_AUTOMERGE)
|| (!showMetadata && isMetadata(projectCtl, name))) {
continue;
} else if (RefNames.isRefsEdit(name)) {
// Edits are visible only to the owning user, if change is visible.
@ -138,6 +139,12 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
if (viewMetadata) {
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()) {
// Use the leaf to lookup the control data. If the reference is
// 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) {
return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name);
private static boolean isMetadata(ProjectControl projectCtl, String name) {
return name.startsWith(REFS_CHANGES)
|| RefNames.isRefsEdit(name)
|| (projectCtl.getProjectState().isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS));
}
private static boolean isTag(Ref ref) {

View File

@ -134,7 +134,8 @@ public class CommitValidators {
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
new ConfigValidator(refControl, repo, allUsers),
new BannedCommitsValidator(rejectCommits),
new PluginCommitValidationListener(pluginValidators)));
new PluginCommitValidationListener(pluginValidators),
new BlockExternalIdUpdateListener(allUsers)));
}
}
@ -149,7 +150,8 @@ public class CommitValidators {
new ChangeIdValidator(
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
new ConfigValidator(refControl, repo, allUsers),
new PluginCommitValidationListener(pluginValidators)));
new PluginCommitValidationListener(pluginValidators),
new BlockExternalIdUpdateListener(allUsers)));
}
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(
RevCommit c,
String type,

View File

@ -18,8 +18,8 @@ import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
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.ExternalId;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil;
@ -42,7 +42,7 @@ public class AccountField {
new FieldDef.Repeatable<AccountState, String>("external_id", FieldType.EXACT, false) {
@Override
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();
Set<String> parts =
SchemaUtil.getNameParts(
fullName,
Iterables.transform(input.getExternalIds(), AccountExternalId::getEmailAddress));
fullName, Iterables.transform(input.getExternalIds(), ExternalId::email));
// Additional values not currently added by getPersonParts.
// TODO(dborowitz): Move to getPersonParts and remove this hack.
@ -87,7 +86,7 @@ public class AccountField {
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
return FluentIterable.from(input.getExternalIds())
.transform(AccountExternalId::getEmailAddress)
.transform(ExternalId::email)
.append(Collections.singleton(input.getAccount().getPreferredEmail()))
.filter(Predicates.notNull())
.transform(String::toLowerCase)

View File

@ -18,6 +18,7 @@ import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.Project;
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.account.AccountIndexCollection;
import com.google.gerrit.server.query.InternalQuery;
@ -67,11 +68,27 @@ public class InternalAccountQuery extends InternalQuery<AccountState> {
return query(AccountPredicates.defaultPredicate(query));
}
public List<AccountState> byExternalId(String externalId) throws OrmException {
return query(AccountPredicates.externalId(externalId));
public List<AccountState> byEmailPrefix(String emailPrefix) throws OrmException {
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 {
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);
if (accountStates.size() == 1) {
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(
ProjectConfig config,
AccessSection section,

View File

@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
public static final Class<Schema_141> C = Schema_141.class;
public static final Class<Schema_142> C = Schema_142.class;
public static int getBinaryVersion() {
return guessVersion(C);

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
fi
curl --digest -u $gerrituser -w '%{http_code}' -o preview \
curl -u $gerrituser -w '%{http_code}' -o preview \
$server/a/changes/$changeId/revisions/current/preview_submit?format=tgz >http_code
if ! grep 200 http_code >/dev/null
then

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.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.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 java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import org.junit.Before;
@ -388,9 +383,6 @@ public class FromAddressGeneratorProviderTest {
account.setFullName(name);
account.setPreferredEmail(email);
return new AccountState(
account,
Collections.<AccountGroup.UUID>emptySet(),
Collections.<AccountExternalId>emptySet(),
new HashMap<ProjectWatchKey, Set<NotifyType>>());
account, Collections.emptySet(), Collections.emptySet(), new HashMap<>());
}
}

View File

@ -17,15 +17,10 @@ package com.google.gerrit.testutil;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil;
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.AccountState;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/** Fake implementation of {@link AccountCache} for testing. */
public class FakeAccountCache implements AccountCache {
@ -81,10 +76,6 @@ public class FakeAccountCache implements AccountCache {
}
private static AccountState newState(Account account) {
return new AccountState(
account,
ImmutableSet.<AccountGroup.UUID>of(),
ImmutableSet.<AccountExternalId>of(),
new HashMap<ProjectWatchKey, Set<NotifyType>>());
return new AccountState(account, ImmutableSet.of(), ImmutableSet.of(), new HashMap<>());
}
}

View File

@ -14,13 +14,13 @@
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.LoadingCache;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountSshKey;
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.cache.CacheModule;
import com.google.gerrit.server.ssh.SshKeyCache;
@ -103,14 +103,17 @@ public class SshKeyCacheImpl implements SshKeyCache {
@Override
public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_USERNAME, username);
AccountExternalId user = db.accountExternalIds().get(key);
ExternalId user =
ExternalId.from(
db.accountExternalIds()
.get(
ExternalId.Key.create(SCHEME_USERNAME, username).asAccountExternalIdKey()));
if (user == null) {
return NO_SUCH_USER;
}
List<SshKeyCacheEntry> kl = new ArrayList<>(4);
for (AccountSshKey k : authorizedKeys.getKeys(user.getAccountId())) {
for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) {
if (k.isValid()) {
add(kl, k);
}

View File

@ -263,7 +263,7 @@ final class SetAccountCommand extends SshCommand {
}
private void addEmail(String email)
throws UnloggedFailure, RestApiException, OrmException, IOException {
throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException {
EmailInput in = new EmailInput();
in.email = email;
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")) {
List<EmailInfo> emails = getEmails.apply(rsrc);
for (EmailInfo e : emails) {

@ -1 +1 @@
Subproject commit 85083bc964ca00437e8695ec7335df9b87f28465
Subproject commit e6d7594621d87859a0e6af361cac1fc3173c3588