// 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.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.gerrit.server.util.TimeUtil; 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 = TimeUtil.nowMs(); 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 Long.compare(a1.getTime(), a2.getTime()); } }); return groupAgreements; } finally { rs.close(); } } finally { stmt.close(); } } }