| // 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; |
| |
| import static java.util.Objects.requireNonNull; |
| import static java.util.stream.Collectors.toList; |
| |
| import com.google.common.collect.Sets; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.data.AccessSection; |
| import com.google.gerrit.common.data.Permission; |
| import com.google.gerrit.common.data.PermissionRule; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.events.ChangeMergedListener; |
| import com.google.gerrit.server.config.AllProjectsName; |
| import com.google.gerrit.server.config.AllUsersName; |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectConfig; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.HashSet; |
| import java.util.Set; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| |
| /** |
| * With groups in NoteDb, the capability of creating a group is expressed as a {@code CREATE} |
| * permission on {@code refs/groups/*} rather than a global capability in {@code All-Projects}. |
| * |
| * <p>During the transition phase, we have to keep these permissions in sync with the global |
| * capabilities that serve as the source of truth. |
| * |
| * <p>This class implements a one-way synchronization from the global {@code CREATE_GROUP} |
| * capability in {@code All-Projects} to a {@code CREATE} permission on {@code refs/groups/*} in |
| * {@code All-Users}. |
| */ |
| @Singleton |
| public class CreateGroupPermissionSyncer implements ChangeMergedListener { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final AllProjectsName allProjects; |
| private final AllUsersName allUsers; |
| private final ProjectCache projectCache; |
| private final Provider<MetaDataUpdate.Server> metaDataUpdateFactory; |
| private final ProjectConfig.Factory projectConfigFactory; |
| |
| @Inject |
| CreateGroupPermissionSyncer( |
| AllProjectsName allProjects, |
| AllUsersName allUsers, |
| ProjectCache projectCache, |
| Provider<MetaDataUpdate.Server> metaDataUpdateFactory, |
| ProjectConfig.Factory projectConfigFactory) { |
| this.allProjects = allProjects; |
| this.allUsers = allUsers; |
| this.projectCache = projectCache; |
| this.metaDataUpdateFactory = metaDataUpdateFactory; |
| this.projectConfigFactory = projectConfigFactory; |
| } |
| |
| /** |
| * Checks if {@code GlobalCapability.CREATE_GROUP} and {@code CREATE} permission on {@code |
| * refs/groups/*} have diverged and syncs them by applying the {@code CREATE} permission to {@code |
| * refs/groups/*}. |
| */ |
| public void syncIfNeeded() throws IOException, ConfigInvalidException { |
| ProjectState allProjectsState = projectCache.checkedGet(allProjects); |
| requireNonNull( |
| allProjectsState, () -> String.format("Can't obtain project state for %s", allProjects)); |
| ProjectState allUsersState = projectCache.checkedGet(allUsers); |
| requireNonNull( |
| allUsersState, () -> String.format("Can't obtain project state for %s", allUsers)); |
| |
| Set<PermissionRule> createGroupsGlobal = |
| new HashSet<>(allProjectsState.getCapabilityCollection().createGroup); |
| Set<PermissionRule> createGroupsRef = new HashSet<>(); |
| |
| AccessSection allUsersCreateGroupAccessSection = |
| allUsersState.getConfig().getAccessSection(RefNames.REFS_GROUPS + "*"); |
| if (allUsersCreateGroupAccessSection != null) { |
| Permission create = allUsersCreateGroupAccessSection.getPermission(Permission.CREATE); |
| if (create != null && create.getRules() != null) { |
| createGroupsRef.addAll(create.getRules()); |
| } |
| } |
| |
| if (Sets.symmetricDifference(createGroupsGlobal, createGroupsRef).isEmpty()) { |
| // Nothing to sync |
| return; |
| } |
| |
| try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsers)) { |
| ProjectConfig config = projectConfigFactory.read(md); |
| AccessSection createGroupAccessSection = |
| config.getAccessSection(RefNames.REFS_GROUPS + "*", true); |
| if (createGroupsGlobal.isEmpty()) { |
| createGroupAccessSection.setPermissions( |
| createGroupAccessSection.getPermissions().stream() |
| .filter(p -> !Permission.CREATE.equals(p.getName())) |
| .collect(toList())); |
| config.replace(createGroupAccessSection); |
| } else { |
| Permission createGroupPermission = new Permission(Permission.CREATE); |
| createGroupAccessSection.addPermission(createGroupPermission); |
| createGroupsGlobal.forEach(createGroupPermission::add); |
| // The create permission is managed by Gerrit at this point only so there is no concern of |
| // overwriting user-defined permissions here. |
| config.replace(createGroupAccessSection); |
| } |
| |
| config.commit(md); |
| projectCache.evict(config.getProject()); |
| } |
| } |
| |
| @Override |
| public void onChangeMerged(Event event) { |
| if (!allProjects.get().equals(event.getChange().project) |
| || !RefNames.REFS_CONFIG.equals(event.getChange().branch)) { |
| return; |
| } |
| try { |
| syncIfNeeded(); |
| } catch (IOException | ConfigInvalidException e) { |
| logger.atSevere().withCause(e).log("Can't sync create group permissions"); |
| } |
| } |
| } |