| // Copyright (C) 2020 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.permissions; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.gerrit.entities.RefNames.REFS_CACHE_AUTOMERGE; |
| |
| import com.google.common.base.Throwables; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.NoSuchGroupException; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.account.GroupControl; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import javax.inject.Inject; |
| import javax.inject.Singleton; |
| import org.eclipse.jgit.lib.Constants; |
| |
| /** |
| * This class is a component that is internal to {@link DefaultPermissionBackend}. It can |
| * authoritatively tell if a ref is accessible by a user. |
| */ |
| @Singleton |
| class RefVisibilityControl { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final PermissionBackend permissionBackend; |
| private final GroupControl.GenericFactory groupControlFactory; |
| private final ChangeData.Factory changeDataFactory; |
| |
| @Inject |
| RefVisibilityControl( |
| PermissionBackend permissionBackend, |
| GroupControl.GenericFactory groupControlFactory, |
| ChangeData.Factory changeDataFactory) { |
| this.permissionBackend = permissionBackend; |
| this.groupControlFactory = groupControlFactory; |
| this.changeDataFactory = changeDataFactory; |
| } |
| |
| /** |
| * Returns an authoritative answer if the ref is visible to the user. Does not have support for |
| * tags and will throw a {@link PermissionBackendException} if asked for tags visibility. |
| */ |
| boolean isVisible(ProjectControl projectControl, String refName) |
| throws PermissionBackendException { |
| if (refName.startsWith(Constants.R_TAGS)) { |
| throw new PermissionBackendException( |
| "can't check tags through RefVisibilityControl. Use PermissionBackend#filter instead."); |
| } |
| if (!RefNames.isGerritRef(refName)) { |
| // This is not a special Gerrit ref and not a NoteDb ref. Likely, it's just a ref under |
| // refs/heads or another ref the user created. Apply the regular permissions with inheritance. |
| return projectControl.controlForRef(refName).hasReadPermissionOnRef(false); |
| } |
| |
| if (refName.startsWith(REFS_CACHE_AUTOMERGE)) { |
| // Internal cache state that is accessible to no one. |
| return false; |
| } |
| |
| boolean hasAccessDatabase = |
| permissionBackend |
| .user(projectControl.getUser()) |
| .testOrFalse(GlobalPermission.ACCESS_DATABASE); |
| if (hasAccessDatabase) { |
| return true; |
| } |
| |
| // Change and change edit visibility |
| Change.Id changeId; |
| if ((changeId = Change.Id.fromRef(refName)) != null) { |
| // Change ref is visible only if the change is visible. |
| ChangeData cd; |
| try { |
| cd = changeDataFactory.create(projectControl.getProject().getNameKey(), changeId); |
| checkState(cd.change().getId().equals(changeId)); |
| } catch (StorageException e) { |
| if (Throwables.getCausalChain(e).stream() |
| .anyMatch(e2 -> e2 instanceof NoSuchChangeException)) { |
| // The change was deleted or is otherwise not accessible anymore. |
| // If the caller can see all refs and is allowed to see private changes on refs/, allow |
| // access. This is an escape hatch for receivers of "ref deleted" events. |
| PermissionBackend.ForProject forProject = projectControl.asForProject(); |
| return forProject.test(ProjectPermission.READ) |
| && forProject.ref("refs/").test(RefPermission.READ_PRIVATE_CHANGES); |
| } |
| throw new PermissionBackendException(e); |
| } |
| if (RefNames.isRefsEdit(refName)) { |
| // Edits are visible only to the owning user, if change is visible. |
| return visibleEdit(refName, projectControl, cd); |
| } |
| return projectControl.controlFor(cd).isVisible(); |
| } |
| |
| // Account visibility |
| CurrentUser user = projectControl.getUser(); |
| Account.Id currentUserAccountId = user.isIdentifiedUser() ? user.getAccountId() : null; |
| Account.Id accountId; |
| if ((accountId = Account.Id.fromRef(refName)) != null) { |
| // Account ref is visible only to the corresponding account. |
| if (accountId.equals(currentUserAccountId)) { |
| // Always allow visibility to refs/draft-comments and refs/starred-changes. For all other |
| // refs, check if the user has read permissions. |
| if (RefNames.isRefsDraftsComments(refName) |
| || RefNames.isRefsStarredChanges(refName) |
| || projectControl.controlForRef(refName).hasReadPermissionOnRef(true)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Group visibility |
| AccountGroup.UUID accountGroupUuid; |
| if ((accountGroupUuid = AccountGroup.UUID.fromRef(refName)) != null) { |
| // Group ref is visible only to the corresponding owner group. |
| try { |
| return projectControl.controlForRef(refName).hasReadPermissionOnRef(true) |
| && groupControlFactory.controlFor(user, accountGroupUuid).isOwner(); |
| } catch (NoSuchGroupException e) { |
| // The group is broken, but the ref is still around. Pretend the ref is not visible. |
| logger.atWarning().withCause(e).log("Found group ref %s but group isn't parsable", refName); |
| return false; |
| } |
| } |
| |
| // We are done checking all cases where we would allow access to Gerrit-managed refs. Deny |
| // access in case we got this far. |
| logger.atFine().log( |
| "Denying access to %s because user doesn't have access to this Gerrit ref", refName); |
| return false; |
| } |
| |
| private boolean visibleEdit(String refName, ProjectControl projectControl, ChangeData cd) |
| throws PermissionBackendException { |
| Change.Id id = Change.Id.fromEditRefPart(refName); |
| if (id == null) { |
| throw new IllegalStateException("unable to parse change id from edit ref " + refName); |
| } |
| |
| if (!projectControl.controlFor(cd).isVisible()) { |
| // The user can't see the change so they can't see any edits. |
| return false; |
| } |
| |
| if (projectControl.getUser().isIdentifiedUser() |
| && refName.startsWith( |
| RefNames.refsEditPrefix(projectControl.getUser().asIdentifiedUser().getAccountId()))) { |
| logger.atFinest().log("Own change edit ref is visible: %s", refName); |
| return true; |
| } |
| |
| // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits. |
| boolean canRead = |
| projectControl |
| .asForProject() |
| .ref(cd.change().getDest().branch()) |
| .test(RefPermission.READ_PRIVATE_CHANGES); |
| logger.atFinest().log( |
| "Foreign change edit ref is " + (canRead ? "visible" : "invisible") + ": %s", refName); |
| return canRead; |
| } |
| } |