diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java index c3b2908921..66b1848e3e 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java @@ -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; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java index 5c8e3e9f3c..b14491fb90 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java @@ -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> { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java index 4d78a7df85..d692e59681 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java @@ -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 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); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java index 5e72327487..70bdb3f4c1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java @@ -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 OWNER_UUID = exact("owner_uuid").build(g -> g.getOwnerGroupUUID().get()); + /** Timestamp indicating when this group was created. */ + public static final FieldDef CREATED_ON = + timestamp("created_on").build(AccountGroup::getCreatedOn); + /** Group name. */ public static final FieldDef NAME = exact("name").build(AccountGroup::getName); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java index 6ba46cb4ef..b05553978f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java @@ -32,7 +32,9 @@ public class GroupSchemaDefinitions extends SchemaDefinitions { GroupField.DESCRIPTION, GroupField.IS_VISIBLE_TO_ALL); - static final Schema V2 = schema(V1); + @Deprecated static final Schema V2 = schema(V1); + + static final Schema V3 = schema(V2, GroupField.CREATED_ON); public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java index 62d0f42b40..26ff0d4a09 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java @@ -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 { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java index 76decec9cf..b68c81a676 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java @@ -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 C = Schema_150.class; + public static final Class C = Schema_151.class; public static int getBinaryVersion() { return guessVersion(C); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_151.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_151.java new file mode 100644 index 0000000000..48545f080b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_151.java @@ -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 prior) { + super(prior); + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException { + List accountGroups = db.accountGroups().all().toList(); + for (AccountGroup accountGroup : accountGroups) { + ResultSet groupMemberAudits = + db.accountGroupMembersAudit().byGroup(accountGroup.getId()); + Optional 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); + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java new file mode 100644 index 0000000000..aea7857c34 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java @@ -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 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 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 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 pruneList) throws OrmException {} + } +}