blob: 1cdf25cdea7d9b651d4fe4f01fbd23548c68ea02 [file] [log] [blame]
// Copyright (C) 2012 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.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Longs;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.GroupUUID;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.SystemReader;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
public class Schema_65 extends SchemaVersion {
private final AllProjectsName allProjects;
private final GitRepositoryManager mgr;
private final PersonIdent serverUser;
private final @AnonymousCowardName String anonymousCowardName;
@Inject
Schema_65(Provider<Schema_64> prior,
AllProjectsName allProjects,
GitRepositoryManager mgr,
@GerritPersonIdent PersonIdent serverUser,
@AnonymousCowardName String anonymousCowardName) {
super(prior);
this.allProjects = allProjects;
this.mgr = mgr;
this.serverUser = serverUser;
this.anonymousCowardName = anonymousCowardName;
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui)
throws OrmException, SQLException {
Repository git;
try {
git = mgr.openRepository(allProjects);
} catch (IOException e) {
throw new OrmException(e);
}
try {
MetaDataUpdate md =
new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
ProjectConfig config = ProjectConfig.read(md);
Map<Integer, ContributorAgreement> agreements = getAgreementToAdd(db, config);
if (agreements.isEmpty()) {
return;
}
ui.message("Moved contributor agreements to project.config");
// Create the auto verify groups.
List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(db, config);
for (ContributorAgreement agreement : agreements.values()) {
if (agreement.getAutoVerify() != null) {
getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
}
}
// Scan AccountAgreement
long minTime = addAccountAgreements(db, config, adminGroupUUIDs, agreements);
ProjectConfig base = ProjectConfig.read(md, null);
for (ContributorAgreement agreement : agreements.values()) {
base.replace(agreement);
}
base.getAccountsSection().setSameGroupVisibility(
config.getAccountsSection().getSameGroupVisibility());
BatchMetaDataUpdate batch = base.openUpdate(md);
try {
// Scan AccountGroupAgreement
List<AccountGroupAgreement> groupAgreements =
getAccountGroupAgreements(db, agreements);
// Find the earliest change
for (AccountGroupAgreement aga : groupAgreements) {
minTime = Math.min(minTime, aga.getTime());
}
minTime -= 60 * 1000; // 1 Minute
CommitBuilder commit = new CommitBuilder();
commit.setAuthor(new PersonIdent(serverUser, new Date(minTime)));
commit.setCommitter(new PersonIdent(serverUser, new Date(minTime)));
commit.setMessage("Add the ContributorAgreements for upgrade to Gerrit Code Review schema 65\n");
batch.write(commit);
for (AccountGroupAgreement aga : groupAgreements) {
AccountGroup group = db.accountGroups().get(aga.groupId);
if (group == null) {
continue;
}
ContributorAgreement agreement = agreements.get(aga.claId);
agreement.getAccepted().add(new PermissionRule(config.resolve(group)));
base.replace(agreement);
PersonIdent ident = null;
if (aga.reviewedBy != null) {
Account ua = db.accounts().get(aga.reviewedBy);
if (ua != null) {
String name = ua.getFullName();
String email = ua.getPreferredEmail();
if (email == null || email.isEmpty()) {
// No preferred email is configured. Use a generic identity so we
// don't leak an address the user may have given us, but doesn't
// necessarily want to publish through Git records.
//
String user = ua.getUserName();
if (user == null || user.isEmpty()) {
user = "account-" + ua.getId().toString();
}
String host = SystemReader.getInstance().getHostname();
email = user + "@" + host;
}
if (name == null || name.isEmpty()) {
final int at = email.indexOf('@');
if (0 < at) {
name = email.substring(0, at);
} else {
name = anonymousCowardName;
}
}
ident = new PersonIdent(name, email, new Date(aga.getTime()), TimeZone.getDefault());
}
}
if (ident == null) {
ident = new PersonIdent(serverUser, new Date(aga.getTime()));
}
// Build the commits such that it keeps track of the date added and
// who added it.
commit = new CommitBuilder();
commit.setAuthor(ident);
commit.setCommitter(new PersonIdent(serverUser, new Date(aga.getTime())));
String msg = String.format("Accept %s contributor agreement for %s\n",
agreement.getName(), group.getName());
if (!Strings.isNullOrEmpty(aga.reviewComments)) {
msg += "\n" + aga.reviewComments + "\n";
}
commit.setMessage(msg);
batch.write(commit);
}
// Merge the agreements with the other data in project.config.
commit = new CommitBuilder();
commit.setAuthor(serverUser);
commit.setCommitter(serverUser);
commit.setMessage("Upgrade to Gerrit Code Review schema 65\n");
commit.addParentId(config.getRevision());
batch.write(config, commit);
// Save the the final metadata.
batch.commitAt(config.getRevision());
} finally {
batch.close();
}
} catch (IOException e) {
throw new OrmException(e);
} catch (ConfigInvalidException e) {
throw new OrmException(e);
} finally {
git.close();
}
}
private Map<Integer, ContributorAgreement> getAgreementToAdd(
ReviewDb db, ProjectConfig config) throws SQLException {
Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
try {
ResultSet rs = stmt.executeQuery(
"SELECT short_name, id, require_contact_information," +
" short_description, agreement_url, auto_verify " +
"FROM contributor_agreements WHERE active = 'Y'");
try {
Map<Integer, ContributorAgreement> agreements = Maps.newHashMap();
while (rs.next()) {
String name = rs.getString(1);
if (config.getContributorAgreement(name) != null) {
continue; // already exists
}
ContributorAgreement a = config.getContributorAgreement(name, true);
agreements.put(rs.getInt(2), a);
a.setRequireContactInformation("Y".equals(rs.getString(3)));
a.setDescription(rs.getString(4));
a.setAgreementUrl(rs.getString(5));
if ("Y".equals(rs.getString(6))) {
a.setAutoVerify(new GroupReference(null, null));
}
}
return agreements;
} finally {
rs.close();
}
} finally {
stmt.close();
}
}
private AccountGroup createGroup(ReviewDb db, String groupName,
AccountGroup.UUID adminGroupUUID, String description)
throws OrmException {
final AccountGroup.Id groupId =
new AccountGroup.Id(db.nextAccountGroupId());
final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
final AccountGroup.UUID uuid = GroupUUID.make(groupName, serverUser);
final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
group.setOwnerGroupUUID(adminGroupUUID);
group.setDescription(description);
final AccountGroupName gn = new AccountGroupName(group);
// first insert the group name to validate that the group name hasn't
// already been used to create another group
db.accountGroupNames().insert(Collections.singleton(gn));
db.accountGroups().insert(Collections.singleton(group));
return group;
}
private List<AccountGroup.UUID> getAdministrateServerGroups(
ReviewDb db, ProjectConfig cfg) {
List<PermissionRule> rules = cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
.getPermission(GlobalCapability.ADMINISTRATE_SERVER)
.getRules();
List<AccountGroup.UUID> groups =
Lists.newArrayListWithExpectedSize(rules.size());
for (PermissionRule rule : rules) {
if (rule.getAction() == Action.ALLOW) {
groups.add(rule.getGroup().getUUID());
}
}
if (groups.isEmpty()) {
throw new IllegalStateException("no administrator group found");
}
return groups;
}
private GroupReference getOrCreateGroupForIndividuals(ReviewDb db,
ProjectConfig config, List<AccountGroup.UUID> adminGroupUUIDs,
ContributorAgreement agreement)
throws OrmException {
if (!agreement.getAccepted().isEmpty()) {
return agreement.getAccepted().get(0).getGroup();
}
String name = "CLA Accepted - " + agreement.getName();
AccountGroupName agn =
db.accountGroupNames().get(new AccountGroup.NameKey(name));
AccountGroup ag;
if (agn != null) {
ag = db.accountGroups().get(agn.getId());
if (ag == null) {
throw new IllegalStateException(
"account group name exists but account group does not: " + name);
}
if (!adminGroupUUIDs.contains(ag.getOwnerGroupUUID())) {
throw new IllegalStateException(
"individual group exists with non admin owner group: " + name);
}
} else {
ag = createGroup(db, name, adminGroupUUIDs.get(0),
String.format("Users who have accepted the %s CLA", agreement.getName()));
}
GroupReference group = config.resolve(ag);
agreement.setAccepted(Lists.newArrayList(new PermissionRule(group)));
if (agreement.getAutoVerify() != null) {
agreement.setAutoVerify(group);
}
// Don't allow accounts in the same individual CLA group to see each
// other in same group visibility mode.
List<PermissionRule> sameGroupVisibility =
config.getAccountsSection().getSameGroupVisibility();
PermissionRule rule = new PermissionRule(group);
rule.setDeny();
if (!sameGroupVisibility.contains(rule)) {
sameGroupVisibility.add(rule);
}
return group;
}
private long addAccountAgreements(ReviewDb db, ProjectConfig config,
List<AccountGroup.UUID> adminGroupUUIDs,
Map<Integer, ContributorAgreement> agreements)
throws SQLException, OrmException {
Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
try {
ResultSet rs = stmt.executeQuery(
"SELECT account_id, cla_id, accepted_on, reviewed_by," +
" reviewed_on, review_comments " +
"FROM account_agreements WHERE status = 'V'");
try {
long minTime = System.currentTimeMillis();
while (rs.next()) {
Account.Id accountId = new Account.Id(rs.getInt(1));
Account.Id reviewerId = new Account.Id(rs.getInt(4));
if (rs.wasNull()) {
reviewerId = accountId;
}
int claId = rs.getInt(2);
ContributorAgreement agreement = agreements.get(claId);
if (agreement == null) {
continue; // Agreement is invalid
}
Timestamp acceptedOn = rs.getTimestamp(3);
minTime = Math.min(minTime, acceptedOn.getTime());
// Enter Agreement
GroupReference individualGroup =
getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
AccountGroup.Id groupId = db.accountGroups()
.byUUID(individualGroup.getUUID())
.toList()
.get(0)
.getId();
final AccountGroupMember.Key key =
new AccountGroupMember.Key(accountId, groupId);
AccountGroupMember m = db.accountGroupMembers().get(key);
if (m == null) {
m = new AccountGroupMember(key);
db.accountGroupMembersAudit().insert(
Collections.singleton(
new AccountGroupMemberAudit(m, reviewerId, acceptedOn)));
db.accountGroupMembers().insert(Collections.singleton(m));
}
}
return minTime;
} finally {
rs.close();
}
} finally {
stmt.close();
}
}
private static class AccountGroupAgreement {
private AccountGroup.Id groupId;
private int claId;
private Timestamp acceptedOn;
private Account.Id reviewedBy;
private Timestamp reviewedOn;
private String reviewComments;
private long getTime() {
return (reviewedOn == null) ? acceptedOn.getTime() : reviewedOn.getTime();
}
}
private List<AccountGroupAgreement> getAccountGroupAgreements(
ReviewDb db, Map<Integer, ContributorAgreement> agreements)
throws SQLException {
Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
try {
ResultSet rs = stmt.executeQuery(
"SELECT group_id, cla_id, accepted_on, reviewed_by, reviewed_on, " +
" review_comments " +
"FROM account_group_agreements");
try {
List<AccountGroupAgreement> groupAgreements = Lists.newArrayList();
while (rs.next()) {
AccountGroupAgreement a = new AccountGroupAgreement();
a.groupId = new AccountGroup.Id(rs.getInt(1));
a.claId = rs.getInt(2);
if (!agreements.containsKey(a.claId)) {
continue; // Agreement is invalid
}
a.acceptedOn = rs.getTimestamp(3);
a.reviewedBy = new Account.Id(rs.getInt(4));
if (rs.wasNull()) {
a.reviewedBy = null;
}
a.reviewedOn = rs.getTimestamp(5);
if (rs.wasNull()) {
a.reviewedOn = null;
}
a.reviewComments = rs.getString(6);
if (rs.wasNull()) {
a.reviewComments = null;
}
groupAgreements.add(a);
}
Collections.sort(groupAgreements, new Comparator<AccountGroupAgreement>() {
@Override
public int compare(
AccountGroupAgreement a1, AccountGroupAgreement a2) {
return Longs.compare(a1.getTime(), a2.getTime());
}
});
return groupAgreements;
} finally {
rs.close();
}
} finally {
stmt.close();
}
}
}