| // Copyright (C) 2008 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.project; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import static com.google.gerrit.entities.PermissionRule.Action.ALLOW; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.collect.FluentIterable; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.entities.AccessSection; | |
| import com.google.gerrit.entities.AccountGroup; | |
| import com.google.gerrit.entities.BooleanProjectConfig; | |
| import com.google.gerrit.entities.BranchNameKey; | |
| import com.google.gerrit.entities.BranchOrderSection; | |
| import com.google.gerrit.entities.CachedProjectConfig; | |
| import com.google.gerrit.entities.GroupReference; | |
| import com.google.gerrit.entities.LabelType; | |
| import com.google.gerrit.entities.LabelTypes; | |
| import com.google.gerrit.entities.Permission; | |
| import com.google.gerrit.entities.PermissionRule; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.StoredCommentLinkInfo; | |
| import com.google.gerrit.entities.SubmitRequirement; | |
| import com.google.gerrit.entities.SubscribeSection; | |
| import com.google.gerrit.extensions.api.projects.CommentLinkInfo; | |
| import com.google.gerrit.extensions.client.SubmitType; | |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; | |
| import com.google.gerrit.index.project.ProjectData; | |
| import com.google.gerrit.server.account.CapabilityCollection; | |
| import com.google.gerrit.server.config.AllProjectsName; | |
| import com.google.gerrit.server.config.AllUsersName; | |
| import com.google.gerrit.server.config.PluginConfig; | |
| import com.google.gerrit.server.git.TransferConfig; | |
| import com.google.gerrit.server.notedb.ChangeNotes; | |
| import com.google.inject.Inject; | |
| import com.google.inject.assistedinject.Assisted; | |
| import java.util.ArrayList; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.HashSet; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.lib.Config; | |
| /** | |
| * State of a project, aggregated from the project and its parents. This is obtained from the {@link | |
| * ProjectCache}. It should not be persisted across requests | |
| */ | |
| public class ProjectState { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| public interface Factory { | |
| ProjectState create(CachedProjectConfig config); | |
| } | |
| private final boolean isAllProjects; | |
| private final boolean isAllUsers; | |
| private final AllProjectsName allProjectsName; | |
| private final ProjectCache projectCache; | |
| private final List<CommentLinkInfo> commentLinks; | |
| private final CachedProjectConfig cachedConfig; | |
| private final Set<AccountGroup.UUID> localOwners; | |
| private final long globalMaxObjectSizeLimit; | |
| private final boolean inheritProjectMaxObjectSizeLimit; | |
| /** Local access sections, wrapped in SectionMatchers for faster evaluation. */ | |
| private volatile List<SectionMatcher> localAccessSections; | |
| /** If this is all projects, the capabilities used by the server. */ | |
| private final CapabilityCollection capabilities; | |
| @Inject | |
| public ProjectState( | |
| ProjectCache projectCache, | |
| AllProjectsName allProjectsName, | |
| AllUsersName allUsersName, | |
| List<CommentLinkInfo> commentLinks, | |
| CapabilityCollection.Factory limitsFactory, | |
| TransferConfig transferConfig, | |
| @Assisted CachedProjectConfig cachedProjectConfig) { | |
| this.projectCache = projectCache; | |
| this.isAllProjects = cachedProjectConfig.getProject().getNameKey().equals(allProjectsName); | |
| this.isAllUsers = cachedProjectConfig.getProject().getNameKey().equals(allUsersName); | |
| this.allProjectsName = allProjectsName; | |
| this.commentLinks = commentLinks; | |
| this.cachedConfig = cachedProjectConfig; | |
| this.capabilities = | |
| isAllProjects | |
| ? limitsFactory.create( | |
| cachedProjectConfig | |
| .getAccessSection(AccessSection.GLOBAL_CAPABILITIES) | |
| .orElse(null)) | |
| : null; | |
| this.globalMaxObjectSizeLimit = transferConfig.getMaxObjectSizeLimit(); | |
| this.inheritProjectMaxObjectSizeLimit = transferConfig.inheritProjectMaxObjectSizeLimit(); | |
| if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) { | |
| localOwners = Collections.emptySet(); | |
| } else { | |
| HashSet<AccountGroup.UUID> groups = new HashSet<>(); | |
| Optional<AccessSection> all = cachedProjectConfig.getAccessSection(AccessSection.ALL); | |
| if (all.isPresent()) { | |
| Permission owner = all.get().getPermission(Permission.OWNER); | |
| if (owner != null) { | |
| for (PermissionRule rule : owner.getRules()) { | |
| GroupReference ref = rule.getGroup(); | |
| if (rule.getAction() == ALLOW && ref.getUUID() != null) { | |
| groups.add(ref.getUUID()); | |
| } | |
| } | |
| } | |
| } | |
| localOwners = Collections.unmodifiableSet(groups); | |
| } | |
| } | |
| /** | |
| * Returns cached computation of all global capabilities. This should only be invoked on the state | |
| * from {@link ProjectCache#getAllProjects()}. Null on any other project. | |
| */ | |
| public CapabilityCollection getCapabilityCollection() { | |
| return capabilities; | |
| } | |
| /** | |
| * Returns true if the Prolog engine is expected to run for this project, that is if this project | |
| * or a parent possesses a rules.pl file. | |
| */ | |
| public boolean hasPrologRules() { | |
| // We check if this project has a rules.pl file | |
| if (getConfig().getRulesId().isPresent()) { | |
| return true; | |
| } | |
| // If not, we check the parents. | |
| return parents().stream() | |
| .map(ProjectState::getConfig) | |
| .map(CachedProjectConfig::getRulesId) | |
| .anyMatch(Optional::isPresent); | |
| } | |
| public Project getProject() { | |
| return cachedConfig.getProject(); | |
| } | |
| public Project.NameKey getNameKey() { | |
| return getProject().getNameKey(); | |
| } | |
| public String getName() { | |
| return getNameKey().get(); | |
| } | |
| public CachedProjectConfig getConfig() { | |
| return cachedConfig; | |
| } | |
| public ProjectLevelConfig getConfig(String fileName) { | |
| checkState(fileName.endsWith(".config"), "file name must end in .config. is: " + fileName); | |
| return new ProjectLevelConfig( | |
| fileName, this, cachedConfig.getParsedProjectLevelConfigs().get(fileName)); | |
| } | |
| public long getMaxObjectSizeLimit() { | |
| return cachedConfig.getMaxObjectSizeLimit(); | |
| } | |
| public boolean statePermitsRead() { | |
| return getProject().getState().permitsRead(); | |
| } | |
| public void checkStatePermitsRead() throws ResourceConflictException { | |
| if (!statePermitsRead()) { | |
| throw new ResourceConflictException( | |
| "project state " + getProject().getState().name() + " does not permit read"); | |
| } | |
| } | |
| public boolean statePermitsWrite() { | |
| return getProject().getState().permitsWrite(); | |
| } | |
| public void checkStatePermitsWrite() throws ResourceConflictException { | |
| if (!statePermitsWrite()) { | |
| throw new ResourceConflictException( | |
| "project state " + getProject().getState().name() + " does not permit write"); | |
| } | |
| } | |
| public static class EffectiveMaxObjectSizeLimit { | |
| public long value; | |
| public String summary; | |
| } | |
| private static final String MAY_NOT_SET = "This project may not set a higher limit."; | |
| @VisibleForTesting | |
| public static final String INHERITED_FROM_PARENT = "Inherited from parent project '%s'."; | |
| @VisibleForTesting | |
| public static final String OVERRIDDEN_BY_PARENT = | |
| "Overridden by parent project '%s'. " + MAY_NOT_SET; | |
| @VisibleForTesting | |
| public static final String INHERITED_FROM_GLOBAL = "Inherited from the global config."; | |
| @VisibleForTesting | |
| public static final String OVERRIDDEN_BY_GLOBAL = | |
| "Overridden by the global config. " + MAY_NOT_SET; | |
| public EffectiveMaxObjectSizeLimit getEffectiveMaxObjectSizeLimit() { | |
| EffectiveMaxObjectSizeLimit result = new EffectiveMaxObjectSizeLimit(); | |
| result.value = cachedConfig.getMaxObjectSizeLimit(); | |
| if (inheritProjectMaxObjectSizeLimit) { | |
| for (ProjectState parent : parents()) { | |
| long parentValue = parent.cachedConfig.getMaxObjectSizeLimit(); | |
| if (parentValue > 0 && result.value > 0) { | |
| if (parentValue < result.value) { | |
| result.value = parentValue; | |
| result.summary = | |
| String.format(OVERRIDDEN_BY_PARENT, parent.cachedConfig.getProject().getNameKey()); | |
| } | |
| } else if (parentValue > 0) { | |
| result.value = parentValue; | |
| result.summary = | |
| String.format(INHERITED_FROM_PARENT, parent.cachedConfig.getProject().getNameKey()); | |
| } | |
| } | |
| } | |
| if (globalMaxObjectSizeLimit > 0 && result.value > 0) { | |
| if (globalMaxObjectSizeLimit < result.value) { | |
| result.value = globalMaxObjectSizeLimit; | |
| result.summary = OVERRIDDEN_BY_GLOBAL; | |
| } | |
| } else if (globalMaxObjectSizeLimit > result.value) { | |
| // zero means "no limit", in this case the max is more limiting | |
| result.value = globalMaxObjectSizeLimit; | |
| result.summary = INHERITED_FROM_GLOBAL; | |
| } | |
| return result; | |
| } | |
| /** Get the sections that pertain only to this project. */ | |
| List<SectionMatcher> getLocalAccessSections() { | |
| if (localAccessSections != null) { | |
| return localAccessSections; | |
| } | |
| List<SectionMatcher> sm = new ArrayList<>(cachedConfig.getAccessSections().values().size()); | |
| for (AccessSection section : cachedConfig.getAccessSections().values()) { | |
| SectionMatcher matcher = SectionMatcher.wrap(getNameKey(), section); | |
| if (matcher != null) { | |
| sm.add(matcher); | |
| } | |
| } | |
| localAccessSections = sm; | |
| return localAccessSections; | |
| } | |
| /** | |
| * Obtain all local and inherited sections. This collection is looked up dynamically and is not | |
| * cached. Callers should try to cache this result per-request as much as possible. | |
| */ | |
| public List<SectionMatcher> getAllSections() { | |
| if (isAllProjects) { | |
| return getLocalAccessSections(); | |
| } | |
| List<SectionMatcher> all = new ArrayList<>(); | |
| for (ProjectState s : tree()) { | |
| all.addAll(s.getLocalAccessSections()); | |
| } | |
| return all; | |
| } | |
| /** | |
| * Returns all {@link AccountGroup}'s to which the owner privilege for 'refs/*' is assigned for | |
| * this project (the local owners), if there are no local owners the local owners of the nearest | |
| * parent project that has local owners are returned | |
| */ | |
| public Set<AccountGroup.UUID> getOwners() { | |
| for (ProjectState p : tree()) { | |
| if (!p.localOwners.isEmpty()) { | |
| return p.localOwners; | |
| } | |
| } | |
| return Collections.emptySet(); | |
| } | |
| /** | |
| * Returns all {@link AccountGroup}'s that are allowed to administrate the complete project. This | |
| * includes all groups to which the owner privilege for 'refs/*' is assigned for this project (the | |
| * local owners) and all groups to which the owner privilege for 'refs/*' is assigned for one of | |
| * the parent projects (the inherited owners). | |
| */ | |
| public Set<AccountGroup.UUID> getAllOwners() { | |
| Set<AccountGroup.UUID> result = new HashSet<>(); | |
| for (ProjectState p : tree()) { | |
| result.addAll(p.localOwners); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Returns an iterable that walks through this project and then the parents of this project. | |
| * Starts from this project and progresses up the hierarchy to All-Projects. | |
| */ | |
| public Iterable<ProjectState> tree() { | |
| return () -> new ProjectHierarchyIterator(projectCache, allProjectsName, ProjectState.this); | |
| } | |
| /** | |
| * Returns an iterable that walks in-order from All-Projects through the project hierarchy to this | |
| * project. | |
| */ | |
| public Iterable<ProjectState> treeInOrder() { | |
| List<ProjectState> projects = Lists.newArrayList(tree()); | |
| Collections.reverse(projects); | |
| return projects; | |
| } | |
| /** | |
| * Returns an iterable that walks through the parents of this project. Starts from the immediate | |
| * parent of this project and progresses up the hierarchy to All-Projects. | |
| */ | |
| public FluentIterable<ProjectState> parents() { | |
| return FluentIterable.from(tree()).skip(1); | |
| } | |
| public boolean isAllProjects() { | |
| return isAllProjects; | |
| } | |
| public boolean isAllUsers() { | |
| return isAllUsers; | |
| } | |
| public boolean is(BooleanProjectConfig config) { | |
| for (ProjectState s : tree()) { | |
| switch (s.getProject().getBooleanConfig(config)) { | |
| case TRUE: | |
| return true; | |
| case FALSE: | |
| return false; | |
| case INHERIT: | |
| default: | |
| continue; | |
| } | |
| } | |
| return false; | |
| } | |
| /** Get all submit requirements for a project, including those from parent projects. */ | |
| public Map<String, SubmitRequirement> getSubmitRequirements() { | |
| Map<String, SubmitRequirement> requirements = new LinkedHashMap<>(); | |
| for (ProjectState s : treeInOrder()) { | |
| for (SubmitRequirement requirement : s.getConfig().getSubmitRequirementSections().values()) { | |
| String lowerName = requirement.name().toLowerCase(Locale.US); | |
| SubmitRequirement old = requirements.get(lowerName); | |
| if (old == null || old.allowOverrideInChildProjects()) { | |
| requirements.put(lowerName, requirement); | |
| } | |
| } | |
| } | |
| return ImmutableMap.copyOf(requirements); | |
| } | |
| /** All available label types. */ | |
| public LabelTypes getLabelTypes() { | |
| Map<String, LabelType> types = new LinkedHashMap<>(); | |
| for (ProjectState s : treeInOrder()) { | |
| for (LabelType type : s.getConfig().getLabelSections().values()) { | |
| String lower = type.getName().toLowerCase(Locale.US); | |
| LabelType old = types.get(lower); | |
| if (old == null || old.isCanOverride()) { | |
| types.put(lower, type); | |
| } | |
| } | |
| } | |
| List<LabelType> all = Lists.newArrayListWithCapacity(types.size()); | |
| for (LabelType type : types.values()) { | |
| if (!type.getValues().isEmpty()) { | |
| all.add(type); | |
| } | |
| } | |
| return new LabelTypes(Collections.unmodifiableList(all)); | |
| } | |
| /** All available label types for this change. */ | |
| public LabelTypes getLabelTypes(ChangeNotes notes) { | |
| return getLabelTypes(notes.getChange().getDest()); | |
| } | |
| /** All available label types for this branch. */ | |
| public LabelTypes getLabelTypes(BranchNameKey destination) { | |
| List<LabelType> all = getLabelTypes().getLabelTypes(); | |
| List<LabelType> r = Lists.newArrayListWithCapacity(all.size()); | |
| for (LabelType l : all) { | |
| List<String> refs = l.getRefPatterns(); | |
| if (refs == null) { | |
| r.add(l); | |
| } else { | |
| for (String refPattern : refs) { | |
| if (refPattern.contains("${")) { | |
| logger.atWarning().log( | |
| "Ref pattern for label %s in project %s contains illegal expanded parameters: %s." | |
| + " Ref pattern will be ignored.", | |
| l, getName(), refPattern); | |
| continue; | |
| } | |
| if (AccessSection.isValidRefSectionName(refPattern) && match(destination, refPattern)) { | |
| r.add(l); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| return new LabelTypes(r); | |
| } | |
| public List<CommentLinkInfo> getCommentLinks() { | |
| Map<String, CommentLinkInfo> cls = new LinkedHashMap<>(); | |
| for (CommentLinkInfo cl : commentLinks) { | |
| cls.put(cl.name.toLowerCase(Locale.US), cl); | |
| } | |
| for (ProjectState s : treeInOrder()) { | |
| for (StoredCommentLinkInfo cl : s.getConfig().getCommentLinkSections().values()) { | |
| String name = cl.getName().toLowerCase(Locale.US); | |
| if (cl.getOverrideOnly()) { | |
| CommentLinkInfo parent = cls.get(name); | |
| if (parent == null) { | |
| continue; // Ignore invalid overrides. | |
| } | |
| cls.put(name, StoredCommentLinkInfo.fromInfo(parent, cl.getEnabled()).toInfo()); | |
| } else { | |
| cls.put(name, cl.toInfo()); | |
| } | |
| } | |
| } | |
| return ImmutableList.copyOf(cls.values()); | |
| } | |
| /** | |
| * Returns the {@link PluginConfig} that got parsed from the {@code plugins} section of {@code | |
| * project.config}. The returned instance is a defensive copy of the cached value. Returns an | |
| * empty config in case we find no config for the given plugin name. This is useful when calling | |
| * {@code PluginConfig#withInheritance(ProjectState.Factory)} | |
| */ | |
| public PluginConfig getPluginConfig(String pluginName) { | |
| Config config = new Config(); | |
| String cachedPluginConfig = getConfig().getPluginConfigs().get(pluginName); | |
| if (cachedPluginConfig != null) { | |
| try { | |
| config.fromText(cachedPluginConfig); | |
| } catch (ConfigInvalidException e) { | |
| // This is OK to propagate as IllegalStateException because it's a programmer error. | |
| // The config was converted to a String using Config#toText. So #fromText must not | |
| // throw a ConfigInvalidException | |
| throw new IllegalStateException("invalid plugin config for " + pluginName, e); | |
| } | |
| } | |
| return PluginConfig.create(pluginName, config, getConfig()); | |
| } | |
| public Optional<BranchOrderSection> getBranchOrderSection() { | |
| for (ProjectState s : tree()) { | |
| Optional<BranchOrderSection> section = s.getConfig().getBranchOrderSection(); | |
| if (section.isPresent()) { | |
| return section; | |
| } | |
| } | |
| return Optional.empty(); | |
| } | |
| public Collection<SubscribeSection> getSubscribeSections(BranchNameKey branch) { | |
| Collection<SubscribeSection> ret = new ArrayList<>(); | |
| for (ProjectState s : tree()) { | |
| ret.addAll(s.getConfig().getSubscribeSections(branch)); | |
| } | |
| return ret; | |
| } | |
| public Set<GroupReference> getAllGroups() { | |
| return getGroups(getAllSections()); | |
| } | |
| public Set<GroupReference> getLocalGroups() { | |
| return getGroups(getLocalAccessSections()); | |
| } | |
| public SubmitType getSubmitType() { | |
| for (ProjectState s : tree()) { | |
| SubmitType t = s.getProject().getSubmitType(); | |
| if (t != SubmitType.INHERIT) { | |
| return t; | |
| } | |
| } | |
| return Project.DEFAULT_ALL_PROJECTS_SUBMIT_TYPE; | |
| } | |
| private static Set<GroupReference> getGroups(List<SectionMatcher> sectionMatcherList) { | |
| final Set<GroupReference> all = new HashSet<>(); | |
| for (SectionMatcher matcher : sectionMatcherList) { | |
| final AccessSection section = matcher.getSection(); | |
| for (Permission permission : section.getPermissions()) { | |
| for (PermissionRule rule : permission.getRules()) { | |
| all.add(rule.getGroup()); | |
| } | |
| } | |
| } | |
| return all; | |
| } | |
| public ProjectData toProjectData() { | |
| ProjectData project = null; | |
| for (ProjectState state : treeInOrder()) { | |
| project = new ProjectData(state.getProject(), Optional.ofNullable(project)); | |
| } | |
| return project; | |
| } | |
| private boolean match(BranchNameKey destination, String refPattern) { | |
| return RefPatternMatcher.getMatcher(refPattern).match(destination.branch(), null); | |
| } | |
| } |