Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server; 16 : 17 : import com.google.common.collect.Sets; 18 : import com.google.common.flogger.FluentLogger; 19 : import com.google.gerrit.entities.AccessSection; 20 : import com.google.gerrit.entities.Permission; 21 : import com.google.gerrit.entities.PermissionRule; 22 : import com.google.gerrit.entities.RefNames; 23 : import com.google.gerrit.extensions.events.ChangeMergedListener; 24 : import com.google.gerrit.server.config.AllProjectsName; 25 : import com.google.gerrit.server.config.AllUsersName; 26 : import com.google.gerrit.server.git.meta.MetaDataUpdate; 27 : import com.google.gerrit.server.project.ProjectCache; 28 : import com.google.gerrit.server.project.ProjectConfig; 29 : import com.google.gerrit.server.project.ProjectState; 30 : import com.google.inject.Inject; 31 : import com.google.inject.Provider; 32 : import com.google.inject.Singleton; 33 : import java.io.IOException; 34 : import java.util.HashSet; 35 : import java.util.Optional; 36 : import java.util.Set; 37 : import org.eclipse.jgit.errors.ConfigInvalidException; 38 : 39 : /** 40 : * With groups in NoteDb, the capability of creating a group is expressed as a {@code CREATE} 41 : * permission on {@code refs/groups/*} rather than a global capability in {@code All-Projects}. 42 : * 43 : * <p>During the transition phase, we have to keep these permissions in sync with the global 44 : * capabilities that serve as the source of truth. 45 : * 46 : * <p>This class implements a one-way synchronization from the global {@code CREATE_GROUP} 47 : * capability in {@code All-Projects} to a {@code CREATE} permission on {@code refs/groups/*} in 48 : * {@code All-Users}. 49 : */ 50 : @Singleton 51 : public class CreateGroupPermissionSyncer implements ChangeMergedListener { 52 146 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 53 : 54 : private final AllProjectsName allProjects; 55 : private final AllUsersName allUsers; 56 : private final ProjectCache projectCache; 57 : private final Provider<MetaDataUpdate.Server> metaDataUpdateFactory; 58 : private final ProjectConfig.Factory projectConfigFactory; 59 : 60 : @Inject 61 : CreateGroupPermissionSyncer( 62 : AllProjectsName allProjects, 63 : AllUsersName allUsers, 64 : ProjectCache projectCache, 65 : Provider<MetaDataUpdate.Server> metaDataUpdateFactory, 66 146 : ProjectConfig.Factory projectConfigFactory) { 67 146 : this.allProjects = allProjects; 68 146 : this.allUsers = allUsers; 69 146 : this.projectCache = projectCache; 70 146 : this.metaDataUpdateFactory = metaDataUpdateFactory; 71 146 : this.projectConfigFactory = projectConfigFactory; 72 146 : } 73 : 74 : /** 75 : * Checks if {@code GlobalCapability.CREATE_GROUP} and {@code CREATE} permission on {@code 76 : * refs/groups/*} have diverged and syncs them by applying the {@code CREATE} permission to {@code 77 : * refs/groups/*}. 78 : */ 79 : public void syncIfNeeded() throws IOException, ConfigInvalidException { 80 8 : ProjectState allProjectsState = projectCache.getAllProjects(); 81 8 : ProjectState allUsersState = projectCache.getAllUsers(); 82 : 83 8 : Set<PermissionRule> createGroupsGlobal = 84 8 : new HashSet<>(allProjectsState.getCapabilityCollection().createGroup); 85 8 : Set<PermissionRule> createGroupsRef = new HashSet<>(); 86 : 87 8 : Optional<AccessSection> allUsersCreateGroupAccessSection = 88 8 : allUsersState.getConfig().getAccessSection(RefNames.REFS_GROUPS + "*"); 89 8 : if (allUsersCreateGroupAccessSection.isPresent()) { 90 8 : Permission create = allUsersCreateGroupAccessSection.get().getPermission(Permission.CREATE); 91 8 : if (create != null && create.getRules() != null) { 92 1 : createGroupsRef.addAll(create.getRules()); 93 : } 94 : } 95 : 96 8 : if (Sets.symmetricDifference(createGroupsGlobal, createGroupsRef).isEmpty()) { 97 : // Nothing to sync 98 8 : return; 99 : } 100 : 101 1 : try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsers)) { 102 1 : ProjectConfig config = projectConfigFactory.read(md); 103 1 : config.upsertAccessSection( 104 : RefNames.REFS_GROUPS + "*", 105 : refsGroupsAccessSectionBuilder -> { 106 1 : if (createGroupsGlobal.isEmpty()) { 107 1 : refsGroupsAccessSectionBuilder.modifyPermissions( 108 : permissions -> { 109 1 : permissions.removeIf(p -> Permission.CREATE.equals(p.getName())); 110 1 : }); 111 : } else { 112 : // The create permission is managed by Gerrit at this point only so there is no 113 : // concern of overwriting user-defined permissions here. 114 1 : Permission.Builder createGroupPermission = Permission.builder(Permission.CREATE); 115 1 : refsGroupsAccessSectionBuilder.remove(createGroupPermission); 116 1 : refsGroupsAccessSectionBuilder.addPermission(createGroupPermission); 117 1 : createGroupsGlobal.stream() 118 1 : .map(p -> p.toBuilder()) 119 1 : .forEach(createGroupPermission::add); 120 : } 121 1 : }); 122 : 123 1 : config.commit(md); 124 1 : projectCache.evictAndReindex(config.getProject()); 125 : } 126 1 : } 127 : 128 : @Override 129 : public void onChangeMerged(Event event) { 130 55 : if (!allProjects.get().equals(event.getChange().project) 131 0 : || !RefNames.REFS_CONFIG.equals(event.getChange().branch)) { 132 55 : return; 133 : } 134 : try { 135 0 : syncIfNeeded(); 136 0 : } catch (IOException | ConfigInvalidException e) { 137 0 : logger.atSevere().withCause(e).log("Can't sync create group permissions"); 138 0 : } 139 0 : } 140 : }