Add 'created on' field to groups

Contrary to accounts, groups currently have no indication on when
they were created. From now on, the instant a group is added will
be remembered. For existing groups, only a best guess is possible by
deriving the creation time from the first time this group was recorded
in the audit. As that is not possible for groups which were created
before the audit was introduced, those groups get the moment the audit
was available as creation time.

Change-Id: Ic5b3cb8ec6373ad804535b73cbbf85675ab3ec6f
This commit is contained in:
Alice Kober-Sotzek
2017-05-31 15:41:52 +02:00
parent afbb4187f5
commit 57a41bc931
9 changed files with 277 additions and 8 deletions

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.reviewdb.client;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.IntKey;
import com.google.gwtorm.client.StringKey;
import java.sql.Timestamp;
/** Named group of one or more accounts, typically used for access controls. */
public final class AccountGroup {
@@ -145,17 +146,22 @@ public final class AccountGroup {
@Column(id = 10)
protected UUID ownerGroupUUID;
@Column(id = 11)
protected Timestamp createdOn;
protected AccountGroup() {}
public AccountGroup(
final AccountGroup.NameKey newName,
final AccountGroup.Id newId,
final AccountGroup.UUID uuid) {
AccountGroup.NameKey newName,
AccountGroup.Id newId,
AccountGroup.UUID uuid,
Timestamp createdOn) {
name = newName;
groupId = newId;
visibleToAll = false;
groupUUID = uuid;
ownerGroupUUID = groupUUID;
this.createdOn = createdOn;
}
public AccountGroup.Id getId() {
@@ -205,4 +211,12 @@ public final class AccountGroup {
public void setGroupUUID(AccountGroup.UUID uuid) {
groupUUID = uuid;
}
public Timestamp getCreatedOn() {
return createdOn;
}
public void setCreatedOn(Timestamp createdOn) {
this.createdOn = createdOn;
}
}

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server.account;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -167,7 +168,7 @@ public class GroupCacheImpl implements GroupCache {
private static AccountGroup missing(AccountGroup.Id key) {
AccountGroup.NameKey name = new AccountGroup.NameKey("Deleted Group" + key);
return new AccountGroup(name, key, null);
return new AccountGroup(name, key, null, TimeUtil.nowTs());
}
static class ByIdLoader extends CacheLoader<AccountGroup.Id, Optional<AccountGroup>> {

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.server.group;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupDescriptions;
@@ -188,7 +189,8 @@ public class CreateGroup implements RestModifyView<TopLevelResource, GroupInput>
GroupUUID.make(
createGroupArgs.getGroupName(),
self.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone()));
AccountGroup group = new AccountGroup(createGroupArgs.getGroup(), groupId, uuid);
AccountGroup group =
new AccountGroup(createGroupArgs.getGroup(), groupId, uuid, TimeUtil.nowTs());
group.setVisibleToAll(createGroupArgs.visibleToAll);
if (createGroupArgs.ownerGroupId != null) {
AccountGroup ownerGroup = groupCache.get(createGroupArgs.ownerGroupId);

View File

@@ -18,10 +18,12 @@ import static com.google.gerrit.server.index.FieldDef.exact;
import static com.google.gerrit.server.index.FieldDef.fullText;
import static com.google.gerrit.server.index.FieldDef.integer;
import static com.google.gerrit.server.index.FieldDef.prefix;
import static com.google.gerrit.server.index.FieldDef.timestamp;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.SchemaUtil;
import java.sql.Timestamp;
/** Secondary index schemas for groups. */
public class GroupField {
@@ -37,6 +39,10 @@ public class GroupField {
public static final FieldDef<AccountGroup, String> OWNER_UUID =
exact("owner_uuid").build(g -> g.getOwnerGroupUUID().get());
/** Timestamp indicating when this group was created. */
public static final FieldDef<AccountGroup, Timestamp> CREATED_ON =
timestamp("created_on").build(AccountGroup::getCreatedOn);
/** Group name. */
public static final FieldDef<AccountGroup, String> NAME =
exact("name").build(AccountGroup::getName);

View File

@@ -32,7 +32,9 @@ public class GroupSchemaDefinitions extends SchemaDefinitions<AccountGroup> {
GroupField.DESCRIPTION,
GroupField.IS_VISIBLE_TO_ALL);
static final Schema<AccountGroup> V2 = schema(V1);
@Deprecated static final Schema<AccountGroup> V2 = schema(V1);
static final Schema<AccountGroup> V3 = schema(V2, GroupField.CREATED_ON);
public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.schema;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
@@ -124,7 +125,8 @@ public class SchemaCreator {
return new AccountGroup( //
new AccountGroup.NameKey(name), //
new AccountGroup.Id(c.nextAccountGroupId()), //
uuid);
uuid,
TimeUtil.nowTs());
}
private SystemConfig initSystemConfig(ReviewDb db) throws OrmException {

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_150> C = Schema_150.class;
public static final Class<Schema_151> C = Schema_151.class;
public static int getBinaryVersion() {
return guessVersion(C);

View File

@@ -0,0 +1,65 @@
// 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.common.annotations.VisibleForTesting;
import com.google.common.collect.Streams;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit.Key;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
/** A schema which adds the 'created on' field to groups. */
public class Schema_151 extends SchemaVersion {
@VisibleForTesting
static final Instant AUDIT_CREATION_INSTANT =
LocalDateTime.of(2009, Month.JUNE, 8, 19, 31).toInstant(ZoneOffset.UTC);
@Inject
protected Schema_151(Provider<Schema_150> prior) {
super(prior);
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
List<AccountGroup> accountGroups = db.accountGroups().all().toList();
for (AccountGroup accountGroup : accountGroups) {
ResultSet<AccountGroupMemberAudit> groupMemberAudits =
db.accountGroupMembersAudit().byGroup(accountGroup.getId());
Optional<Timestamp> firstTimeMentioned =
Streams.stream(groupMemberAudits)
.map(AccountGroupMemberAudit::getKey)
.map(Key::getAddedOn)
.min(Comparator.naturalOrder());
Timestamp createdOn =
firstTimeMentioned.orElseGet(() -> Timestamp.from(AUDIT_CREATION_INSTANT));
accountGroup.setCreatedOn(createdOn);
}
db.accountGroups().update(accountGroups);
}
}

View File

@@ -0,0 +1,177 @@
// 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 static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroup.Id;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
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.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.group.CreateGroup;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.testutil.InMemoryDatabase;
import com.google.gerrit.testutil.InMemoryModule;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.gwtorm.server.SchemaFactory;
import com.google.gwtorm.server.StatementExecutor;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.util.Providers;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class Schema_150_to_151_Test {
@Inject private AccountManager accountManager;
@Inject private IdentifiedUser.GenericFactory userFactory;
@Inject private SchemaFactory<ReviewDb> schemaFactory;
@Inject private SchemaCreator schemaCreator;
@Inject private ThreadLocalRequestContext requestContext;
@Inject private Schema_151 schema151;
@Inject private CreateGroup.Factory createGroupFactory;
// Only for use in setting up/tearing down injector.
@Inject private InMemoryDatabase inMemoryDatabase;
private LifecycleManager lifecycle;
private ReviewDb db;
@Before
public void setUp() throws Exception {
Injector injector = Guice.createInjector(new InMemoryModule());
injector.injectMembers(this);
lifecycle = new LifecycleManager();
lifecycle.add(injector);
lifecycle.start();
try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
schemaCreator.create(underlyingDb);
}
db = schemaFactory.open();
Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
IdentifiedUser user = userFactory.create(userId);
requestContext.setContext(
new RequestContext() {
@Override
public CurrentUser getUser() {
return user;
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return Providers.of(db);
}
});
}
@After
public void tearDown() {
if (lifecycle != null) {
lifecycle.stop();
}
requestContext.setContext(null);
if (db != null) {
db.close();
}
InMemoryDatabase.drop(inMemoryDatabase);
}
@Test
public void createdOnIsPopulatedForGroupsCreatedAfterAudit() throws Exception {
Timestamp testStartTime = TimeUtil.nowTs();
AccountGroup.Id groupId = createGroup("Group for schema migration");
setCreatedOnToVeryOldTimestamp(groupId);
schema151.migrateData(db, new TestUpdateUI());
AccountGroup group = db.accountGroups().get(groupId);
assertThat(group.getCreatedOn()).isAtLeast(testStartTime);
}
@Test
public void createdOnIsPopulatedForGroupsCreatedBeforeAudit() throws Exception {
AccountGroup.Id groupId = createGroup("Ancient group for schema migration");
setCreatedOnToVeryOldTimestamp(groupId);
removeAuditEntriesFor(groupId);
schema151.migrateData(db, new TestUpdateUI());
AccountGroup group = db.accountGroups().get(groupId);
assertThat(group.getCreatedOn()).isEqualTo(Timestamp.from(Schema_151.AUDIT_CREATION_INSTANT));
}
private AccountGroup.Id createGroup(String name) throws Exception {
GroupInput groupInput = new GroupInput();
groupInput.name = name;
GroupInfo groupInfo =
createGroupFactory.create(name).apply(TopLevelResource.INSTANCE, groupInput);
return new Id(groupInfo.groupId);
}
private void setCreatedOnToVeryOldTimestamp(Id groupId) throws OrmException {
AccountGroup group = db.accountGroups().get(groupId);
Instant instant = LocalDateTime.of(1800, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC);
group.setCreatedOn(Timestamp.from(instant));
db.accountGroups().update(ImmutableList.of(group));
}
private void removeAuditEntriesFor(AccountGroup.Id groupId) throws Exception {
ResultSet<AccountGroupMemberAudit> groupMemberAudits =
db.accountGroupMembersAudit().byGroup(groupId);
db.accountGroupMembersAudit().delete(groupMemberAudits);
}
private static class TestUpdateUI implements UpdateUI {
@Override
public void message(String msg) {}
@Override
public boolean yesno(boolean def, String msg) {
return false;
}
@Override
public boolean isBatch() {
return false;
}
@Override
public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {}
}
}