| // 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(); |
| } |
| } |
| } |