Line data Source code
1 : // Copyright (C) 2020 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.permissions; 16 : 17 : import static com.google.common.base.Preconditions.checkState; 18 : import static com.google.gerrit.entities.RefNames.REFS_CACHE_AUTOMERGE; 19 : 20 : import com.google.common.base.Throwables; 21 : import com.google.common.flogger.FluentLogger; 22 : import com.google.gerrit.entities.Account; 23 : import com.google.gerrit.entities.AccountGroup; 24 : import com.google.gerrit.entities.Change; 25 : import com.google.gerrit.entities.RefNames; 26 : import com.google.gerrit.exceptions.NoSuchGroupException; 27 : import com.google.gerrit.exceptions.StorageException; 28 : import com.google.gerrit.server.CurrentUser; 29 : import com.google.gerrit.server.account.GroupControl; 30 : import com.google.gerrit.server.project.NoSuchChangeException; 31 : import com.google.gerrit.server.query.change.ChangeData; 32 : import javax.inject.Inject; 33 : import javax.inject.Singleton; 34 : import org.eclipse.jgit.lib.Constants; 35 : 36 : /** 37 : * This class is a component that is internal to {@link DefaultPermissionBackend}. It can 38 : * authoritatively tell if a ref is accessible by a user. 39 : */ 40 : @Singleton 41 : class RefVisibilityControl { 42 148 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 43 : 44 : private final PermissionBackend permissionBackend; 45 : private final GroupControl.GenericFactory groupControlFactory; 46 : private final ChangeData.Factory changeDataFactory; 47 : 48 : @Inject 49 : RefVisibilityControl( 50 : PermissionBackend permissionBackend, 51 : GroupControl.GenericFactory groupControlFactory, 52 148 : ChangeData.Factory changeDataFactory) { 53 148 : this.permissionBackend = permissionBackend; 54 148 : this.groupControlFactory = groupControlFactory; 55 148 : this.changeDataFactory = changeDataFactory; 56 148 : } 57 : 58 : /** 59 : * Returns an authoritative answer if the ref is visible to the user. Does not have support for 60 : * tags and will throw a {@link PermissionBackendException} if asked for tags visibility. 61 : */ 62 : boolean isVisible(ProjectControl projectControl, String refName) 63 : throws PermissionBackendException { 64 131 : if (refName.startsWith(Constants.R_TAGS)) { 65 0 : throw new PermissionBackendException( 66 : "can't check tags through RefVisibilityControl. Use PermissionBackend#filter instead."); 67 : } 68 131 : if (!RefNames.isGerritRef(refName)) { 69 : // This is not a special Gerrit ref and not a NoteDb ref. Likely, it's just a ref under 70 : // refs/heads or another ref the user created. Apply the regular permissions with inheritance. 71 126 : return projectControl.controlForRef(refName).hasReadPermissionOnRef(false); 72 : } 73 : 74 102 : if (refName.startsWith(REFS_CACHE_AUTOMERGE)) { 75 : // Internal cache state that is accessible to no one. 76 26 : return false; 77 : } 78 : 79 102 : boolean hasAccessDatabase = 80 : permissionBackend 81 102 : .user(projectControl.getUser()) 82 102 : .testOrFalse(GlobalPermission.ACCESS_DATABASE); 83 102 : if (hasAccessDatabase) { 84 9 : return true; 85 : } 86 : 87 : // Change and change edit visibility 88 : Change.Id changeId; 89 102 : if ((changeId = Change.Id.fromRef(refName)) != null) { 90 : // Change ref is visible only if the change is visible. 91 : ChangeData cd; 92 : try { 93 92 : cd = changeDataFactory.create(projectControl.getProject().getNameKey(), changeId); 94 92 : checkState(cd.change().getId().equals(changeId)); 95 8 : } catch (StorageException e) { 96 8 : if (Throwables.getCausalChain(e).stream() 97 8 : .anyMatch(e2 -> e2 instanceof NoSuchChangeException)) { 98 : // The change was deleted or is otherwise not accessible anymore. 99 : // If the caller can see all refs and is allowed to see private changes on refs/, allow 100 : // access. This is an escape hatch for receivers of "ref deleted" events. 101 8 : PermissionBackend.ForProject forProject = projectControl.asForProject(); 102 8 : return forProject.test(ProjectPermission.READ) 103 8 : && forProject.ref("refs/").test(RefPermission.READ_PRIVATE_CHANGES); 104 : } 105 0 : throw new PermissionBackendException(e); 106 92 : } 107 92 : if (RefNames.isRefsEdit(refName)) { 108 : // Edits are visible only to the owning user, if change is visible. 109 18 : return visibleEdit(refName, projectControl, cd); 110 : } 111 92 : return projectControl.controlFor(cd).isVisible(); 112 : } 113 : 114 : // Account visibility 115 102 : CurrentUser user = projectControl.getUser(); 116 102 : Account.Id currentUserAccountId = user.isIdentifiedUser() ? user.getAccountId() : null; 117 : Account.Id accountId; 118 102 : if ((accountId = Account.Id.fromRef(refName)) != null) { 119 : // Account ref is visible only to the corresponding account. 120 45 : if (accountId.equals(currentUserAccountId)) { 121 : // Always allow visibility to refs/draft-comments and refs/starred-changes. For all other 122 : // refs, check if the user has read permissions. 123 19 : if (RefNames.isRefsDraftsComments(refName) 124 19 : || RefNames.isRefsStarredChanges(refName) 125 19 : || projectControl.controlForRef(refName).hasReadPermissionOnRef(true)) { 126 19 : return true; 127 : } 128 : } 129 42 : return false; 130 : } 131 : 132 : // Group visibility 133 : AccountGroup.UUID accountGroupUuid; 134 102 : if ((accountGroupUuid = AccountGroup.UUID.fromRef(refName)) != null) { 135 : // Group ref is visible only to the corresponding owner group. 136 : try { 137 35 : return projectControl.controlForRef(refName).hasReadPermissionOnRef(true) 138 35 : && groupControlFactory.controlFor(user, accountGroupUuid).isOwner(); 139 1 : } catch (NoSuchGroupException e) { 140 : // The group is broken, but the ref is still around. Pretend the ref is not visible. 141 1 : logger.atWarning().withCause(e).log("Found group ref %s but group isn't parsable", refName); 142 1 : return false; 143 : } 144 : } 145 : 146 : // We are done checking all cases where we would allow access to Gerrit-managed refs. Deny 147 : // access in case we got this far. 148 101 : logger.atFine().log( 149 : "Denying access to %s because user doesn't have access to this Gerrit ref", refName); 150 101 : return false; 151 : } 152 : 153 : private boolean visibleEdit(String refName, ProjectControl projectControl, ChangeData cd) 154 : throws PermissionBackendException { 155 18 : Change.Id id = Change.Id.fromEditRefPart(refName); 156 18 : if (id == null) { 157 0 : throw new IllegalStateException("unable to parse change id from edit ref " + refName); 158 : } 159 : 160 18 : if (!projectControl.controlFor(cd).isVisible()) { 161 : // The user can't see the change so they can't see any edits. 162 1 : return false; 163 : } 164 : 165 18 : if (projectControl.getUser().isIdentifiedUser() 166 18 : && refName.startsWith( 167 18 : RefNames.refsEditPrefix(projectControl.getUser().asIdentifiedUser().getAccountId()))) { 168 16 : logger.atFinest().log("Own change edit ref is visible: %s", refName); 169 16 : return true; 170 : } 171 : 172 : // Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits. 173 4 : boolean canRead = 174 : projectControl 175 4 : .asForProject() 176 4 : .ref(cd.change().getDest().branch()) 177 4 : .test(RefPermission.READ_PRIVATE_CHANGES); 178 4 : logger.atFinest().log( 179 4 : "Foreign change edit ref is " + (canRead ? "visible" : "invisible") + ": %s", refName); 180 4 : return canRead; 181 : } 182 : }