blob: 0b4fd0299a2e1aa2df64177152e4465b9df0bb8a [file] [log] [blame]
// 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.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.git.meta.VersionedMetaData.BatchMetaDataUpdate;
import com.google.gerrit.server.group.db.AuditLogFormatter;
import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gerrit.server.group.db.InternalGroupCreation;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
import com.google.gerrit.server.group.db.InternalGroupUpdate.MemberModification;
import com.google.gerrit.server.group.db.InternalGroupUpdate.SubgroupModification;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
/** Helper for rebuilding an entire group's NoteDb refs. */
class GroupRebuilder {
private final PersonIdent serverIdent;
private final AllUsersName allUsers;
private final AuditLogFormatter auditLogFormatter;
public GroupRebuilder(
PersonIdent serverIdent, AllUsersName allUsers, AuditLogFormatter auditLogFormatter) {
this.serverIdent = serverIdent;
this.allUsers = allUsers;
this.auditLogFormatter = auditLogFormatter;
}
public void rebuild(Repository allUsersRepo, GroupBundle bundle, @Nullable BatchRefUpdate bru)
throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
AccountGroup group = bundle.group();
InternalGroupCreation groupCreation =
InternalGroupCreation.builder()
.setId(bundle.id())
.setNameKey(group.getNameKey())
.setGroupUUID(group.getGroupUUID())
.build();
GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsers, allUsersRepo, groupCreation);
groupConfig.setAllowSaveEmptyName();
InternalGroupUpdate.Builder updateBuilder =
InternalGroupUpdate.builder()
.setOwnerGroupUUID(group.getOwnerGroupUUID())
.setVisibleToAll(group.isVisibleToAll())
.setUpdatedOn(group.getCreatedOn());
if (bundle.group().getDescription() != null) {
updateBuilder.setDescription(group.getDescription());
}
groupConfig.setGroupUpdate(updateBuilder.build(), auditLogFormatter);
Map<Key, Collection<Event>> events = toEvents(bundle).asMap();
PersonIdent nowServerIdent = getServerIdent(events);
MetaDataUpdate md = createMetaDataUpdate(allUsers, allUsersRepo, bru);
// Creation is done by the server (unlike later audit events).
PersonIdent created = new PersonIdent(nowServerIdent, group.getCreatedOn());
md.getCommitBuilder().setAuthor(created);
md.getCommitBuilder().setCommitter(created);
// Rebuild group ref.
try (BatchMetaDataUpdate batch = groupConfig.openUpdate(md)) {
batch.write(groupConfig, md.getCommitBuilder());
for (Map.Entry<Key, Collection<Event>> e : events.entrySet()) {
InternalGroupUpdate.Builder ub = InternalGroupUpdate.builder();
e.getValue().forEach(event -> event.update().accept(ub));
ub.setUpdatedOn(e.getKey().when());
groupConfig.setGroupUpdate(ub.build(), auditLogFormatter);
PersonIdent currServerIdent = new PersonIdent(nowServerIdent, e.getKey().when());
CommitBuilder cb = new CommitBuilder();
cb.setAuthor(
e.getKey()
.accountId()
.map(id -> auditLogFormatter.getParsableAuthorIdent(id, currServerIdent))
.orElse(currServerIdent));
cb.setCommitter(currServerIdent);
batch.write(groupConfig, cb);
}
batch.createRef(groupConfig.getRefName());
}
}
private ListMultimap<Key, Event> toEvents(GroupBundle bundle) {
ListMultimap<Key, Event> result =
MultimapBuilder.treeKeys(Key.COMPARATOR).arrayListValues(1).build();
Event e;
for (AccountGroupMemberAudit a : bundle.memberAudit()) {
checkArgument(
a.getKey().getGroupId().equals(bundle.id()),
"key %s does not match group %s",
a.getKey(),
bundle.id());
Account.Id accountId = a.getKey().getParentKey();
e = event(Type.ADD_MEMBER, a.getAddedBy(), a.getKey().getAddedOn(), addMember(accountId));
result.put(e.key(), e);
if (!a.isActive()) {
e = event(Type.REMOVE_MEMBER, a.getRemovedBy(), a.getRemovedOn(), removeMember(accountId));
result.put(e.key(), e);
}
}
for (AccountGroupByIdAud a : bundle.byIdAudit()) {
checkArgument(
a.getKey().getParentKey().equals(bundle.id()),
"key %s does not match group %s",
a.getKey(),
bundle.id());
AccountGroup.UUID uuid = a.getKey().getIncludeUUID();
e = event(Type.ADD_GROUP, a.getAddedBy(), a.getKey().getAddedOn(), addGroup(uuid));
result.put(e.key(), e);
if (!a.isActive()) {
e = event(Type.REMOVE_GROUP, a.getRemovedBy(), a.getRemovedOn(), removeGroup(uuid));
result.put(e.key(), e);
}
}
// Due to clock skew, audit events may be in the future relative to this machine. Ensure the
// fixup event happens after any other events, both for the purposes of sorting Keys correctly
// and to avoid non-monotonic timestamps in the commit history.
Timestamp maxTs =
Stream.concat(result.keySet().stream().map(Key::when), Stream.of(TimeUtil.nowTs()))
.max(Comparator.naturalOrder())
.get();
Timestamp fixupTs = new Timestamp(maxTs.getTime() + 1);
e = serverEvent(Type.FIXUP, fixupTs, setCurrentMembership(bundle));
result.put(e.key(), e);
return result;
}
private PersonIdent getServerIdent(Map<Key, Collection<Event>> events) {
// Created with MultimapBuilder.treeKeys, so the keySet is navigable.
Key lastKey = ((NavigableSet<Key>) events.keySet()).last();
checkState(lastKey.type() == Type.FIXUP);
return new PersonIdent(
serverIdent.getName(),
serverIdent.getEmailAddress(),
Iterables.getOnlyElement(events.get(lastKey)).when(),
serverIdent.getTimeZone());
}
private static MetaDataUpdate createMetaDataUpdate(
Project.NameKey projectName, Repository repository, @Nullable BatchRefUpdate batchRefUpdate) {
return new MetaDataUpdate(
GitReferenceUpdated.DISABLED, projectName, repository, batchRefUpdate);
}
private static Consumer<InternalGroupUpdate.Builder> addMember(Account.Id toAdd) {
return b -> {
MemberModification prev = b.getMemberModification();
b.setMemberModification(in -> Sets.union(prev.apply(in), ImmutableSet.of(toAdd)));
};
}
private static Consumer<InternalGroupUpdate.Builder> removeMember(Account.Id toRemove) {
return b -> {
MemberModification prev = b.getMemberModification();
b.setMemberModification(in -> Sets.difference(prev.apply(in), ImmutableSet.of(toRemove)));
};
}
private static Consumer<InternalGroupUpdate.Builder> addGroup(AccountGroup.UUID toAdd) {
return b -> {
SubgroupModification prev = b.getSubgroupModification();
b.setSubgroupModification(in -> Sets.union(prev.apply(in), ImmutableSet.of(toAdd)));
};
}
private static Consumer<InternalGroupUpdate.Builder> removeGroup(AccountGroup.UUID toRemove) {
return b -> {
SubgroupModification prev = b.getSubgroupModification();
b.setSubgroupModification(in -> Sets.difference(prev.apply(in), ImmutableSet.of(toRemove)));
};
}
private static Consumer<InternalGroupUpdate.Builder> setCurrentMembership(GroupBundle bundle) {
// Overwrite members and subgroups with the current values. The storage layer will do the
// set differences to compute the appropriate delta, if any.
return b ->
b.setMemberModification(
in ->
bundle.members().stream()
.map(AccountGroupMember::getAccountId)
.collect(toImmutableSet()))
.setSubgroupModification(
in ->
bundle.byId().stream()
.map(AccountGroupById::getIncludeUUID)
.collect(toImmutableSet()));
}
private static Event event(
Type type,
Account.Id accountId,
Timestamp when,
Consumer<InternalGroupUpdate.Builder> update) {
return new AutoValue_GroupRebuilder_Event(type, Optional.of(accountId), when, update);
}
private static Event serverEvent(
Type type, Timestamp when, Consumer<InternalGroupUpdate.Builder> update) {
return new AutoValue_GroupRebuilder_Event(type, Optional.empty(), when, update);
}
@AutoValue
abstract static class Event {
abstract Type type();
abstract Optional<Account.Id> accountId();
abstract Timestamp when();
abstract Consumer<InternalGroupUpdate.Builder> update();
Key key() {
return new AutoValue_GroupRebuilder_Key(accountId(), when(), type());
}
}
/**
* Distinct event types.
*
* <p>Events at the same time by the same user are batched together by type. The types should
* correspond to the possible batch operations supported by AuditService.
*/
enum Type {
ADD_MEMBER,
REMOVE_MEMBER,
ADD_GROUP,
REMOVE_GROUP,
FIXUP;
}
@AutoValue
abstract static class Key {
static final Comparator<Key> COMPARATOR =
Comparator.comparing(Key::when)
.thenComparing(
k -> k.accountId().map(Account.Id::get).orElse(null),
Comparator.nullsFirst(Comparator.naturalOrder()))
.thenComparing(Key::type);
abstract Optional<Account.Id> accountId();
abstract Timestamp when();
abstract Type type();
}
}