Line data Source code
1 : // Copyright (C) 2008 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.project; 16 : 17 : import static com.google.common.base.Preconditions.checkState; 18 : import static com.google.gerrit.entities.PermissionRule.Action.ALLOW; 19 : 20 : import com.google.common.annotations.VisibleForTesting; 21 : import com.google.common.collect.FluentIterable; 22 : import com.google.common.collect.ImmutableList; 23 : import com.google.common.collect.ImmutableMap; 24 : import com.google.common.collect.Lists; 25 : import com.google.common.flogger.FluentLogger; 26 : import com.google.gerrit.entities.AccessSection; 27 : import com.google.gerrit.entities.AccountGroup; 28 : import com.google.gerrit.entities.BooleanProjectConfig; 29 : import com.google.gerrit.entities.BranchNameKey; 30 : import com.google.gerrit.entities.BranchOrderSection; 31 : import com.google.gerrit.entities.CachedProjectConfig; 32 : import com.google.gerrit.entities.GroupReference; 33 : import com.google.gerrit.entities.LabelType; 34 : import com.google.gerrit.entities.LabelTypes; 35 : import com.google.gerrit.entities.Permission; 36 : import com.google.gerrit.entities.PermissionRule; 37 : import com.google.gerrit.entities.Project; 38 : import com.google.gerrit.entities.StoredCommentLinkInfo; 39 : import com.google.gerrit.entities.SubmitRequirement; 40 : import com.google.gerrit.entities.SubscribeSection; 41 : import com.google.gerrit.extensions.api.projects.CommentLinkInfo; 42 : import com.google.gerrit.extensions.client.SubmitType; 43 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 44 : import com.google.gerrit.index.project.ProjectData; 45 : import com.google.gerrit.server.account.CapabilityCollection; 46 : import com.google.gerrit.server.config.AllProjectsName; 47 : import com.google.gerrit.server.config.AllUsersName; 48 : import com.google.gerrit.server.config.PluginConfig; 49 : import com.google.gerrit.server.git.TransferConfig; 50 : import com.google.gerrit.server.notedb.ChangeNotes; 51 : import com.google.inject.Inject; 52 : import com.google.inject.assistedinject.Assisted; 53 : import java.util.ArrayList; 54 : import java.util.Collection; 55 : import java.util.Collections; 56 : import java.util.HashSet; 57 : import java.util.LinkedHashMap; 58 : import java.util.List; 59 : import java.util.Map; 60 : import java.util.Optional; 61 : import java.util.Set; 62 : import org.eclipse.jgit.errors.ConfigInvalidException; 63 : import org.eclipse.jgit.lib.Config; 64 : 65 : /** 66 : * State of a project, aggregated from the project and its parents. This is obtained from the {@link 67 : * ProjectCache}. It should not be persisted across requests 68 : */ 69 : public class ProjectState { 70 153 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 71 : 72 : public interface Factory { 73 : ProjectState create(CachedProjectConfig config); 74 : } 75 : 76 : private final boolean isAllProjects; 77 : private final boolean isAllUsers; 78 : private final AllProjectsName allProjectsName; 79 : private final ProjectCache projectCache; 80 : private final List<CommentLinkInfo> commentLinks; 81 : 82 : private final CachedProjectConfig cachedConfig; 83 : private final Set<AccountGroup.UUID> localOwners; 84 : private final long globalMaxObjectSizeLimit; 85 : private final boolean inheritProjectMaxObjectSizeLimit; 86 : 87 : /** Local access sections, wrapped in SectionMatchers for faster evaluation. */ 88 : private volatile List<SectionMatcher> localAccessSections; 89 : 90 : /** If this is all projects, the capabilities used by the server. */ 91 : private final CapabilityCollection capabilities; 92 : 93 : @Inject 94 : public ProjectState( 95 : ProjectCache projectCache, 96 : AllProjectsName allProjectsName, 97 : AllUsersName allUsersName, 98 : List<CommentLinkInfo> commentLinks, 99 : CapabilityCollection.Factory limitsFactory, 100 : TransferConfig transferConfig, 101 151 : @Assisted CachedProjectConfig cachedProjectConfig) { 102 151 : this.projectCache = projectCache; 103 151 : this.isAllProjects = cachedProjectConfig.getProject().getNameKey().equals(allProjectsName); 104 151 : this.isAllUsers = cachedProjectConfig.getProject().getNameKey().equals(allUsersName); 105 151 : this.allProjectsName = allProjectsName; 106 151 : this.commentLinks = commentLinks; 107 151 : this.cachedConfig = cachedProjectConfig; 108 151 : this.capabilities = 109 151 : isAllProjects 110 151 : ? limitsFactory.create( 111 : cachedProjectConfig 112 151 : .getAccessSection(AccessSection.GLOBAL_CAPABILITIES) 113 151 : .orElse(null)) 114 151 : : null; 115 151 : this.globalMaxObjectSizeLimit = transferConfig.getMaxObjectSizeLimit(); 116 151 : this.inheritProjectMaxObjectSizeLimit = transferConfig.inheritProjectMaxObjectSizeLimit(); 117 : 118 151 : if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) { 119 151 : localOwners = Collections.emptySet(); 120 : } else { 121 148 : HashSet<AccountGroup.UUID> groups = new HashSet<>(); 122 148 : Optional<AccessSection> all = cachedProjectConfig.getAccessSection(AccessSection.ALL); 123 148 : if (all.isPresent()) { 124 61 : Permission owner = all.get().getPermission(Permission.OWNER); 125 61 : if (owner != null) { 126 18 : for (PermissionRule rule : owner.getRules()) { 127 18 : GroupReference ref = rule.getGroup(); 128 18 : if (rule.getAction() == ALLOW && ref.getUUID() != null) { 129 18 : groups.add(ref.getUUID()); 130 : } 131 18 : } 132 : } 133 : } 134 148 : localOwners = Collections.unmodifiableSet(groups); 135 : } 136 151 : } 137 : 138 : /** 139 : * Returns cached computation of all global capabilities. This should only be invoked on the state 140 : * from {@link ProjectCache#getAllProjects()}. Null on any other project. 141 : */ 142 : public CapabilityCollection getCapabilityCollection() { 143 150 : return capabilities; 144 : } 145 : 146 : /** 147 : * Returns true if the Prolog engine is expected to run for this project, that is if this project 148 : * or a parent possesses a rules.pl file. 149 : */ 150 : public boolean hasPrologRules() { 151 : // We check if this project has a rules.pl file 152 103 : if (getConfig().getRulesId().isPresent()) { 153 5 : return true; 154 : } 155 : 156 : // If not, we check the parents. 157 103 : return parents().stream() 158 103 : .map(ProjectState::getConfig) 159 103 : .map(CachedProjectConfig::getRulesId) 160 103 : .anyMatch(Optional::isPresent); 161 : } 162 : 163 : public Project getProject() { 164 148 : return cachedConfig.getProject(); 165 : } 166 : 167 : public Project.NameKey getNameKey() { 168 148 : return getProject().getNameKey(); 169 : } 170 : 171 : public String getName() { 172 82 : return getNameKey().get(); 173 : } 174 : 175 : public CachedProjectConfig getConfig() { 176 151 : return cachedConfig; 177 : } 178 : 179 : public ProjectLevelConfig getConfig(String fileName) { 180 1 : checkState(fileName.endsWith(".config"), "file name must end in .config. is: " + fileName); 181 1 : return new ProjectLevelConfig( 182 1 : fileName, this, cachedConfig.getParsedProjectLevelConfigs().get(fileName)); 183 : } 184 : 185 : public long getMaxObjectSizeLimit() { 186 0 : return cachedConfig.getMaxObjectSizeLimit(); 187 : } 188 : 189 : public boolean statePermitsRead() { 190 145 : return getProject().getState().permitsRead(); 191 : } 192 : 193 : public void checkStatePermitsRead() throws ResourceConflictException { 194 105 : if (!statePermitsRead()) { 195 0 : throw new ResourceConflictException( 196 0 : "project state " + getProject().getState().name() + " does not permit read"); 197 : } 198 105 : } 199 : 200 : public boolean statePermitsWrite() { 201 119 : return getProject().getState().permitsWrite(); 202 : } 203 : 204 : public void checkStatePermitsWrite() throws ResourceConflictException { 205 86 : if (!statePermitsWrite()) { 206 1 : throw new ResourceConflictException( 207 1 : "project state " + getProject().getState().name() + " does not permit write"); 208 : } 209 86 : } 210 : 211 105 : public static class EffectiveMaxObjectSizeLimit { 212 : public long value; 213 : public String summary; 214 : } 215 : 216 : private static final String MAY_NOT_SET = "This project may not set a higher limit."; 217 : 218 : @VisibleForTesting 219 : public static final String INHERITED_FROM_PARENT = "Inherited from parent project '%s'."; 220 : 221 : @VisibleForTesting 222 : public static final String OVERRIDDEN_BY_PARENT = 223 : "Overridden by parent project '%s'. " + MAY_NOT_SET; 224 : 225 : @VisibleForTesting 226 : public static final String INHERITED_FROM_GLOBAL = "Inherited from the global config."; 227 : 228 : @VisibleForTesting 229 : public static final String OVERRIDDEN_BY_GLOBAL = 230 : "Overridden by the global config. " + MAY_NOT_SET; 231 : 232 : public EffectiveMaxObjectSizeLimit getEffectiveMaxObjectSizeLimit() { 233 105 : EffectiveMaxObjectSizeLimit result = new EffectiveMaxObjectSizeLimit(); 234 : 235 105 : result.value = cachedConfig.getMaxObjectSizeLimit(); 236 : 237 105 : if (inheritProjectMaxObjectSizeLimit) { 238 1 : for (ProjectState parent : parents()) { 239 1 : long parentValue = parent.cachedConfig.getMaxObjectSizeLimit(); 240 1 : if (parentValue > 0 && result.value > 0) { 241 1 : if (parentValue < result.value) { 242 1 : result.value = parentValue; 243 1 : result.summary = 244 1 : String.format(OVERRIDDEN_BY_PARENT, parent.cachedConfig.getProject().getNameKey()); 245 : } 246 1 : } else if (parentValue > 0) { 247 1 : result.value = parentValue; 248 1 : result.summary = 249 1 : String.format(INHERITED_FROM_PARENT, parent.cachedConfig.getProject().getNameKey()); 250 : } 251 1 : } 252 : } 253 : 254 105 : if (globalMaxObjectSizeLimit > 0 && result.value > 0) { 255 1 : if (globalMaxObjectSizeLimit < result.value) { 256 1 : result.value = globalMaxObjectSizeLimit; 257 1 : result.summary = OVERRIDDEN_BY_GLOBAL; 258 : } 259 105 : } else if (globalMaxObjectSizeLimit > result.value) { 260 : // zero means "no limit", in this case the max is more limiting 261 1 : result.value = globalMaxObjectSizeLimit; 262 1 : result.summary = INHERITED_FROM_GLOBAL; 263 : } 264 105 : return result; 265 : } 266 : 267 : /** Get the sections that pertain only to this project. */ 268 : List<SectionMatcher> getLocalAccessSections() { 269 145 : if (localAccessSections != null) { 270 0 : return localAccessSections; 271 : } 272 145 : List<SectionMatcher> sm = new ArrayList<>(cachedConfig.getAccessSections().values().size()); 273 145 : for (AccessSection section : cachedConfig.getAccessSections().values()) { 274 145 : SectionMatcher matcher = SectionMatcher.wrap(getNameKey(), section); 275 145 : if (matcher != null) { 276 145 : sm.add(matcher); 277 : } 278 145 : } 279 145 : localAccessSections = sm; 280 145 : return localAccessSections; 281 : } 282 : 283 : /** 284 : * Obtain all local and inherited sections. This collection is looked up dynamically and is not 285 : * cached. Callers should try to cache this result per-request as much as possible. 286 : */ 287 : public List<SectionMatcher> getAllSections() { 288 145 : if (isAllProjects) { 289 103 : return getLocalAccessSections(); 290 : } 291 : 292 145 : List<SectionMatcher> all = new ArrayList<>(); 293 145 : for (ProjectState s : tree()) { 294 145 : all.addAll(s.getLocalAccessSections()); 295 145 : } 296 145 : return all; 297 : } 298 : 299 : /** 300 : * Returns all {@link AccountGroup}'s to which the owner privilege for 'refs/*' is assigned for 301 : * this project (the local owners), if there are no local owners the local owners of the nearest 302 : * parent project that has local owners are returned 303 : */ 304 : public Set<AccountGroup.UUID> getOwners() { 305 1 : for (ProjectState p : tree()) { 306 1 : if (!p.localOwners.isEmpty()) { 307 1 : return p.localOwners; 308 : } 309 0 : } 310 0 : return Collections.emptySet(); 311 : } 312 : 313 : /** 314 : * Returns all {@link AccountGroup}'s that are allowed to administrate the complete project. This 315 : * includes all groups to which the owner privilege for 'refs/*' is assigned for this project (the 316 : * local owners) and all groups to which the owner privilege for 'refs/*' is assigned for one of 317 : * the parent projects (the inherited owners). 318 : */ 319 : public Set<AccountGroup.UUID> getAllOwners() { 320 145 : Set<AccountGroup.UUID> result = new HashSet<>(); 321 : 322 145 : for (ProjectState p : tree()) { 323 145 : result.addAll(p.localOwners); 324 145 : } 325 : 326 145 : return result; 327 : } 328 : 329 : /** 330 : * Returns an iterable that walks through this project and then the parents of this project. 331 : * Starts from this project and progresses up the hierarchy to All-Projects. 332 : */ 333 : public Iterable<ProjectState> tree() { 334 148 : return () -> new ProjectHierarchyIterator(projectCache, allProjectsName, ProjectState.this); 335 : } 336 : 337 : /** 338 : * Returns an iterable that walks in-order from All-Projects through the project hierarchy to this 339 : * project. 340 : */ 341 : public Iterable<ProjectState> treeInOrder() { 342 148 : List<ProjectState> projects = Lists.newArrayList(tree()); 343 148 : Collections.reverse(projects); 344 148 : return projects; 345 : } 346 : 347 : /** 348 : * Returns an iterable that walks through the parents of this project. Starts from the immediate 349 : * parent of this project and progresses up the hierarchy to All-Projects. 350 : */ 351 : public FluentIterable<ProjectState> parents() { 352 110 : return FluentIterable.from(tree()).skip(1); 353 : } 354 : 355 : public boolean isAllProjects() { 356 23 : return isAllProjects; 357 : } 358 : 359 : public boolean isAllUsers() { 360 135 : return isAllUsers; 361 : } 362 : 363 : public boolean is(BooleanProjectConfig config) { 364 115 : for (ProjectState s : tree()) { 365 115 : switch (s.getProject().getBooleanConfig(config)) { 366 : case TRUE: 367 107 : return true; 368 : case FALSE: 369 115 : return false; 370 : case INHERIT: 371 : default: 372 115 : continue; 373 : } 374 : } 375 114 : return false; 376 : } 377 : 378 : /** Get all submit requirements for a project, including those from parent projects. */ 379 : public Map<String, SubmitRequirement> getSubmitRequirements() { 380 103 : Map<String, SubmitRequirement> requirements = new LinkedHashMap<>(); 381 103 : for (ProjectState s : treeInOrder()) { 382 103 : for (SubmitRequirement requirement : s.getConfig().getSubmitRequirementSections().values()) { 383 2 : String lowerName = requirement.name().toLowerCase(); 384 2 : SubmitRequirement old = requirements.get(lowerName); 385 2 : if (old == null || old.allowOverrideInChildProjects()) { 386 2 : requirements.put(lowerName, requirement); 387 : } 388 2 : } 389 103 : } 390 103 : return ImmutableMap.copyOf(requirements); 391 : } 392 : 393 : /** All available label types. */ 394 : public LabelTypes getLabelTypes() { 395 145 : Map<String, LabelType> types = new LinkedHashMap<>(); 396 145 : for (ProjectState s : treeInOrder()) { 397 145 : for (LabelType type : s.getConfig().getLabelSections().values()) { 398 145 : String lower = type.getName().toLowerCase(); 399 145 : LabelType old = types.get(lower); 400 145 : if (old == null || old.isCanOverride()) { 401 145 : types.put(lower, type); 402 : } 403 145 : } 404 145 : } 405 145 : List<LabelType> all = Lists.newArrayListWithCapacity(types.size()); 406 145 : for (LabelType type : types.values()) { 407 145 : if (!type.getValues().isEmpty()) { 408 145 : all.add(type); 409 : } 410 145 : } 411 145 : return new LabelTypes(Collections.unmodifiableList(all)); 412 : } 413 : 414 : /** All available label types for this change. */ 415 : public LabelTypes getLabelTypes(ChangeNotes notes) { 416 103 : return getLabelTypes(notes.getChange().getDest()); 417 : } 418 : 419 : /** All available label types for this branch. */ 420 : public LabelTypes getLabelTypes(BranchNameKey destination) { 421 103 : List<LabelType> all = getLabelTypes().getLabelTypes(); 422 : 423 103 : List<LabelType> r = Lists.newArrayListWithCapacity(all.size()); 424 103 : for (LabelType l : all) { 425 103 : List<String> refs = l.getRefPatterns(); 426 103 : if (refs == null) { 427 103 : r.add(l); 428 : } else { 429 1 : for (String refPattern : refs) { 430 1 : if (refPattern.contains("${")) { 431 0 : logger.atWarning().log( 432 : "Ref pattern for label %s in project %s contains illegal expanded parameters: %s." 433 : + " Ref pattern will be ignored.", 434 0 : l, getName(), refPattern); 435 0 : continue; 436 : } 437 : 438 1 : if (AccessSection.isValidRefSectionName(refPattern) && match(destination, refPattern)) { 439 1 : r.add(l); 440 1 : break; 441 : } 442 1 : } 443 : } 444 103 : } 445 : 446 103 : return new LabelTypes(r); 447 : } 448 : 449 : public List<CommentLinkInfo> getCommentLinks() { 450 22 : Map<String, CommentLinkInfo> cls = new LinkedHashMap<>(); 451 22 : for (CommentLinkInfo cl : commentLinks) { 452 1 : cls.put(cl.name.toLowerCase(), cl); 453 1 : } 454 22 : for (ProjectState s : treeInOrder()) { 455 22 : for (StoredCommentLinkInfo cl : s.getConfig().getCommentLinkSections().values()) { 456 1 : String name = cl.getName().toLowerCase(); 457 1 : if (cl.getOverrideOnly()) { 458 0 : CommentLinkInfo parent = cls.get(name); 459 0 : if (parent == null) { 460 0 : continue; // Ignore invalid overrides. 461 : } 462 0 : cls.put(name, StoredCommentLinkInfo.fromInfo(parent, cl.getEnabled()).toInfo()); 463 0 : } else { 464 1 : cls.put(name, cl.toInfo()); 465 : } 466 1 : } 467 22 : } 468 22 : return ImmutableList.copyOf(cls.values()); 469 : } 470 : 471 : /** 472 : * Returns the {@link PluginConfig} that got parsed from the {@code plugins} section of {@code 473 : * project.config}. The returned instance is a defensive copy of the cached value. Returns an 474 : * empty config in case we find no config for the given plugin name. This is useful when calling 475 : * {@code PluginConfig#withInheritance(ProjectState.Factory)} 476 : */ 477 : public PluginConfig getPluginConfig(String pluginName) { 478 2 : if (getConfig().getPluginConfigs().containsKey(pluginName)) { 479 2 : Config config = new Config(); 480 : try { 481 2 : config.fromText(getConfig().getPluginConfigs().get(pluginName)); 482 0 : } catch (ConfigInvalidException e) { 483 : // This is OK to propagate as IllegalStateException because it's a programmer error. 484 : // The config was converted to a String using Config#toText. So #fromText must not 485 : // throw a ConfigInvalidException 486 0 : throw new IllegalStateException("invalid plugin config for " + pluginName, e); 487 2 : } 488 2 : return PluginConfig.create(pluginName, config, getConfig()); 489 : } 490 2 : return PluginConfig.create(pluginName, new Config(), getConfig()); 491 : } 492 : 493 : public Optional<BranchOrderSection> getBranchOrderSection() { 494 1 : for (ProjectState s : tree()) { 495 1 : Optional<BranchOrderSection> section = s.getConfig().getBranchOrderSection(); 496 1 : if (section.isPresent()) { 497 1 : return section; 498 : } 499 0 : } 500 0 : return Optional.empty(); 501 : } 502 : 503 : public Collection<SubscribeSection> getSubscribeSections(BranchNameKey branch) { 504 65 : Collection<SubscribeSection> ret = new ArrayList<>(); 505 65 : for (ProjectState s : tree()) { 506 65 : ret.addAll(s.getConfig().getSubscribeSections(branch)); 507 65 : } 508 65 : return ret; 509 : } 510 : 511 : public Set<GroupReference> getAllGroups() { 512 0 : return getGroups(getAllSections()); 513 : } 514 : 515 : public Set<GroupReference> getLocalGroups() { 516 1 : return getGroups(getLocalAccessSections()); 517 : } 518 : 519 : public SubmitType getSubmitType() { 520 107 : for (ProjectState s : tree()) { 521 107 : SubmitType t = s.getProject().getSubmitType(); 522 107 : if (t != SubmitType.INHERIT) { 523 107 : return t; 524 : } 525 95 : } 526 0 : return Project.DEFAULT_ALL_PROJECTS_SUBMIT_TYPE; 527 : } 528 : 529 : private static Set<GroupReference> getGroups(List<SectionMatcher> sectionMatcherList) { 530 1 : final Set<GroupReference> all = new HashSet<>(); 531 1 : for (SectionMatcher matcher : sectionMatcherList) { 532 1 : final AccessSection section = matcher.getSection(); 533 1 : for (Permission permission : section.getPermissions()) { 534 1 : for (PermissionRule rule : permission.getRules()) { 535 1 : all.add(rule.getGroup()); 536 1 : } 537 1 : } 538 1 : } 539 1 : return all; 540 : } 541 : 542 : public ProjectData toProjectData() { 543 147 : ProjectData project = null; 544 147 : for (ProjectState state : treeInOrder()) { 545 147 : project = new ProjectData(state.getProject(), Optional.ofNullable(project)); 546 147 : } 547 147 : return project; 548 : } 549 : 550 : private boolean match(BranchNameKey destination, String refPattern) { 551 1 : return RefPatternMatcher.getMatcher(refPattern).match(destination.branch(), null); 552 : } 553 : }