| // 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.gerrit.common.data.PermissionRule.Action.ALLOW; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.collect.FluentIterable; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.gerrit.common.data.AccessSection; |
| import com.google.gerrit.common.data.GroupReference; |
| import com.google.gerrit.common.data.LabelType; |
| import com.google.gerrit.common.data.LabelTypes; |
| import com.google.gerrit.common.data.Permission; |
| import com.google.gerrit.common.data.PermissionRule; |
| import com.google.gerrit.common.data.RefConfigSection; |
| import com.google.gerrit.common.data.SubscribeSection; |
| import com.google.gerrit.extensions.api.projects.CommentLinkInfo; |
| import com.google.gerrit.extensions.api.projects.ThemeInfo; |
| import com.google.gerrit.extensions.client.InheritableBoolean; |
| import com.google.gerrit.reviewdb.client.AccountGroup; |
| import com.google.gerrit.reviewdb.client.Branch; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.client.RefNames; |
| import com.google.gerrit.rules.PrologEnvironment; |
| import com.google.gerrit.rules.RulesCache; |
| import com.google.gerrit.server.CurrentUser; |
| 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.SitePaths; |
| import com.google.gerrit.server.git.BranchOrderSection; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.ProjectConfig; |
| import com.google.gerrit.server.git.ProjectLevelConfig; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| import com.googlecode.prolog_cafe.exceptions.CompileException; |
| import com.googlecode.prolog_cafe.lang.PrologMachineCopy; |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.function.Function; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** Cached information on a project. */ |
| public class ProjectState { |
| private static final Logger log = LoggerFactory.getLogger(ProjectState.class); |
| |
| public interface Factory { |
| ProjectState create(ProjectConfig config); |
| } |
| |
| private final boolean isAllProjects; |
| private final boolean isAllUsers; |
| private final SitePaths sitePaths; |
| private final AllProjectsName allProjectsName; |
| private final ProjectCache projectCache; |
| private final ProjectControl.AssistedFactory projectControlFactory; |
| private final PrologEnvironment.Factory envFactory; |
| private final GitRepositoryManager gitMgr; |
| private final RulesCache rulesCache; |
| private final List<CommentLinkInfo> commentLinks; |
| |
| private final ProjectConfig config; |
| private final Map<String, ProjectLevelConfig> configs; |
| private final Set<AccountGroup.UUID> localOwners; |
| |
| /** Prolog rule state. */ |
| private volatile PrologMachineCopy rulesMachine; |
| |
| /** Last system time the configuration's revision was examined. */ |
| private volatile long lastCheckGeneration; |
| |
| /** Local access sections, wrapped in SectionMatchers for faster evaluation. */ |
| private volatile List<SectionMatcher> localAccessSections; |
| |
| /** Theme information loaded from site_path/themes. */ |
| private volatile ThemeInfo theme; |
| |
| /** If this is all projects, the capabilities used by the server. */ |
| private final CapabilityCollection capabilities; |
| |
| /** All label types applicable to changes in this project. */ |
| private LabelTypes labelTypes; |
| |
| @Inject |
| public ProjectState( |
| final SitePaths sitePaths, |
| final ProjectCache projectCache, |
| final AllProjectsName allProjectsName, |
| final AllUsersName allUsersName, |
| final ProjectControl.AssistedFactory projectControlFactory, |
| final PrologEnvironment.Factory envFactory, |
| final GitRepositoryManager gitMgr, |
| final RulesCache rulesCache, |
| final List<CommentLinkInfo> commentLinks, |
| final CapabilityCollection.Factory limitsFactory, |
| @Assisted final ProjectConfig config) { |
| this.sitePaths = sitePaths; |
| this.projectCache = projectCache; |
| this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName); |
| this.isAllUsers = config.getProject().getNameKey().equals(allUsersName); |
| this.allProjectsName = allProjectsName; |
| this.projectControlFactory = projectControlFactory; |
| this.envFactory = envFactory; |
| this.gitMgr = gitMgr; |
| this.rulesCache = rulesCache; |
| this.commentLinks = commentLinks; |
| this.config = config; |
| this.configs = new HashMap<>(); |
| this.capabilities = |
| isAllProjects |
| ? limitsFactory.create(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)) |
| : null; |
| |
| if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) { |
| localOwners = Collections.emptySet(); |
| } else { |
| HashSet<AccountGroup.UUID> groups = new HashSet<>(); |
| AccessSection all = config.getAccessSection(AccessSection.ALL); |
| if (all != null) { |
| Permission owner = all.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); |
| } |
| } |
| |
| void initLastCheck(long generation) { |
| lastCheckGeneration = generation; |
| } |
| |
| boolean needsRefresh(long generation) { |
| if (generation <= 0) { |
| return isRevisionOutOfDate(); |
| } |
| if (lastCheckGeneration != generation) { |
| lastCheckGeneration = generation; |
| return isRevisionOutOfDate(); |
| } |
| return false; |
| } |
| |
| private boolean isRevisionOutOfDate() { |
| try (Repository git = gitMgr.openRepository(getNameKey())) { |
| Ref ref = git.getRefDatabase().exactRef(RefNames.REFS_CONFIG); |
| if (ref == null || ref.getObjectId() == null) { |
| return true; |
| } |
| return !ref.getObjectId().equals(config.getRevision()); |
| } catch (IOException gone) { |
| return true; |
| } |
| } |
| |
| /** |
| * @return 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; |
| } |
| |
| /** @return Construct a new PrologEnvironment for the calling thread. */ |
| public PrologEnvironment newPrologEnvironment() throws CompileException { |
| PrologMachineCopy pmc = rulesMachine; |
| if (pmc == null) { |
| pmc = rulesCache.loadMachine(getNameKey(), config.getRulesId()); |
| rulesMachine = pmc; |
| } |
| return envFactory.create(pmc); |
| } |
| |
| /** |
| * Like {@link #newPrologEnvironment()} but instead of reading the rules.pl read the provided |
| * input stream. |
| * |
| * @param name a name of the input stream. Could be any name. |
| * @param in stream to read prolog rules from |
| * @throws CompileException |
| */ |
| public PrologEnvironment newPrologEnvironment(String name, Reader in) throws CompileException { |
| PrologMachineCopy pmc = rulesCache.loadMachine(name, in); |
| return envFactory.create(pmc); |
| } |
| |
| public Project getProject() { |
| return config.getProject(); |
| } |
| |
| public Project.NameKey getNameKey() { |
| return getProject().getNameKey(); |
| } |
| |
| public String getName() { |
| return getNameKey().get(); |
| } |
| |
| public ProjectConfig getConfig() { |
| return config; |
| } |
| |
| public ProjectLevelConfig getConfig(String fileName) { |
| if (configs.containsKey(fileName)) { |
| return configs.get(fileName); |
| } |
| |
| ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this); |
| try (Repository git = gitMgr.openRepository(getNameKey())) { |
| cfg.load(git); |
| } catch (IOException | ConfigInvalidException e) { |
| log.warn("Failed to load " + fileName + " for " + getName(), e); |
| } |
| |
| configs.put(fileName, cfg); |
| return cfg; |
| } |
| |
| public long getMaxObjectSizeLimit() { |
| return config.getMaxObjectSizeLimit(); |
| } |
| |
| /** Get the sections that pertain only to this project. */ |
| List<SectionMatcher> getLocalAccessSections() { |
| List<SectionMatcher> sm = localAccessSections; |
| if (sm == null) { |
| Collection<AccessSection> fromConfig = config.getAccessSections(); |
| sm = new ArrayList<>(fromConfig.size()); |
| for (AccessSection section : fromConfig) { |
| if (isAllProjects) { |
| List<Permission> copy = Lists.newArrayListWithCapacity(section.getPermissions().size()); |
| for (Permission p : section.getPermissions()) { |
| if (Permission.canBeOnAllProjects(section.getName(), p.getName())) { |
| copy.add(p); |
| } |
| } |
| section = new AccessSection(section.getName()); |
| section.setPermissions(copy); |
| } |
| |
| SectionMatcher matcher = SectionMatcher.wrap(getNameKey(), section); |
| if (matcher != null) { |
| sm.add(matcher); |
| } |
| } |
| localAccessSections = sm; |
| } |
| return sm; |
| } |
| |
| /** |
| * 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. |
| */ |
| List<SectionMatcher> getAllSections() { |
| if (isAllProjects) { |
| return getLocalAccessSections(); |
| } |
| |
| List<SectionMatcher> all = new ArrayList<>(); |
| for (ProjectState s : tree()) { |
| all.addAll(s.getLocalAccessSections()); |
| } |
| return all; |
| } |
| |
| /** |
| * @return 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(); |
| } |
| |
| /** |
| * @return 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; |
| } |
| |
| public ProjectControl controlFor(CurrentUser user) { |
| return projectControlFactory.create(user, this); |
| } |
| |
| /** |
| * @return 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 Iterable<ProjectState>() { |
| @Override |
| public Iterator<ProjectState> iterator() { |
| return new ProjectHierarchyIterator(projectCache, allProjectsName, ProjectState.this); |
| } |
| }; |
| } |
| |
| /** |
| * @return 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; |
| } |
| |
| /** |
| * @return 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 isUseContributorAgreements() { |
| return getInheritableBoolean(Project::getUseContributorAgreements); |
| } |
| |
| public boolean isUseContentMerge() { |
| return getInheritableBoolean(Project::getUseContentMerge); |
| } |
| |
| public boolean isUseSignedOffBy() { |
| return getInheritableBoolean(Project::getUseSignedOffBy); |
| } |
| |
| public boolean isRequireChangeID() { |
| return getInheritableBoolean(Project::getRequireChangeID); |
| } |
| |
| public boolean isCreateNewChangeForAllNotInTarget() { |
| return getInheritableBoolean(Project::getCreateNewChangeForAllNotInTarget); |
| } |
| |
| public boolean isEnableSignedPush() { |
| return getInheritableBoolean(Project::getEnableSignedPush); |
| } |
| |
| public boolean isRequireSignedPush() { |
| return getInheritableBoolean(Project::getRequireSignedPush); |
| } |
| |
| public boolean isRejectImplicitMerges() { |
| return getInheritableBoolean(Project::getRejectImplicitMerges); |
| } |
| |
| public boolean isPrivateByDefault() { |
| return getInheritableBoolean(Project::getPrivateByDefault); |
| } |
| |
| public boolean isEnableReviewerByEmail() { |
| return getInheritableBoolean(Project::getEnableReviewerByEmail); |
| } |
| |
| public boolean isMatchAuthorToCommitterDate() { |
| return getInheritableBoolean(Project::getMatchAuthorToCommitterDate); |
| } |
| |
| /** All available label types. */ |
| public LabelTypes getLabelTypes() { |
| if (labelTypes == null) { |
| labelTypes = loadLabelTypes(); |
| } |
| return labelTypes; |
| } |
| |
| /** All available label types for this change and user. */ |
| public LabelTypes getLabelTypes(ChangeNotes notes, CurrentUser user) { |
| return getLabelTypes(notes.getChange().getDest(), user); |
| } |
| |
| /** All available label types for this branch and user. */ |
| public LabelTypes getLabelTypes(Branch.NameKey destination, CurrentUser user) { |
| 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 (RefConfigSection.isValid(refPattern) && match(destination, refPattern, user)) { |
| 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(), cl); |
| } |
| for (ProjectState s : treeInOrder()) { |
| for (CommentLinkInfoImpl cl : s.getConfig().getCommentLinkSections()) { |
| String name = cl.name.toLowerCase(); |
| if (cl.isOverrideOnly()) { |
| CommentLinkInfo parent = cls.get(name); |
| if (parent == null) { |
| continue; // Ignore invalid overrides. |
| } |
| cls.put(name, cl.inherit(parent)); |
| } else { |
| cls.put(name, cl); |
| } |
| } |
| } |
| return ImmutableList.copyOf(cls.values()); |
| } |
| |
| public BranchOrderSection getBranchOrderSection() { |
| for (ProjectState s : tree()) { |
| BranchOrderSection section = s.getConfig().getBranchOrderSection(); |
| if (section != null) { |
| return section; |
| } |
| } |
| return null; |
| } |
| |
| public Collection<SubscribeSection> getSubscribeSections(Branch.NameKey branch) { |
| Collection<SubscribeSection> ret = new ArrayList<>(); |
| for (ProjectState s : tree()) { |
| ret.addAll(s.getConfig().getSubscribeSections(branch)); |
| } |
| return ret; |
| } |
| |
| public ThemeInfo getTheme() { |
| ThemeInfo theme = this.theme; |
| if (theme == null) { |
| synchronized (this) { |
| theme = this.theme; |
| if (theme == null) { |
| theme = loadTheme(); |
| this.theme = theme; |
| } |
| } |
| } |
| if (theme == ThemeInfo.INHERIT) { |
| ProjectState parent = Iterables.getFirst(parents(), null); |
| return parent != null ? parent.getTheme() : null; |
| } |
| return theme; |
| } |
| |
| public Set<GroupReference> getAllGroups() { |
| return getGroups(getAllSections()); |
| } |
| |
| public Set<GroupReference> getLocalGroups() { |
| return getGroups(getLocalAccessSections()); |
| } |
| |
| private static Set<GroupReference> getGroups(List<SectionMatcher> sectionMatcherList) { |
| final Set<GroupReference> all = new HashSet<>(); |
| for (SectionMatcher matcher : sectionMatcherList) { |
| final AccessSection section = matcher.section; |
| for (Permission permission : section.getPermissions()) { |
| for (PermissionRule rule : permission.getRules()) { |
| all.add(rule.getGroup()); |
| } |
| } |
| } |
| return all; |
| } |
| |
| private ThemeInfo loadTheme() { |
| String name = getConfig().getProject().getName(); |
| Path dir = sitePaths.themes_dir.resolve(name); |
| if (!Files.exists(dir)) { |
| return ThemeInfo.INHERIT; |
| } else if (!Files.isDirectory(dir)) { |
| log.warn("Bad theme for {}: not a directory", name); |
| return ThemeInfo.INHERIT; |
| } |
| try { |
| return new ThemeInfo( |
| readFile(dir.resolve(SitePaths.CSS_FILENAME)), |
| readFile(dir.resolve(SitePaths.HEADER_FILENAME)), |
| readFile(dir.resolve(SitePaths.FOOTER_FILENAME))); |
| } catch (IOException e) { |
| log.error("Error reading theme for " + name, e); |
| return ThemeInfo.INHERIT; |
| } |
| } |
| |
| private String readFile(Path p) throws IOException { |
| return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null; |
| } |
| |
| private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) { |
| for (ProjectState s : tree()) { |
| switch (func.apply(s.getProject())) { |
| case TRUE: |
| return true; |
| case FALSE: |
| return false; |
| case INHERIT: |
| default: |
| continue; |
| } |
| } |
| return false; |
| } |
| |
| private LabelTypes loadLabelTypes() { |
| Map<String, LabelType> types = new LinkedHashMap<>(); |
| for (ProjectState s : treeInOrder()) { |
| for (LabelType type : s.getConfig().getLabelSections().values()) { |
| String lower = type.getName().toLowerCase(); |
| LabelType old = types.get(lower); |
| if (old == null || old.canOverride()) { |
| 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)); |
| } |
| |
| private boolean match(Branch.NameKey destination, String refPattern, CurrentUser user) { |
| return RefPatternMatcher.getMatcher(refPattern).match(destination.get(), user); |
| } |
| } |