summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/project/CreateRefControl.java
blob: ab134b50ce4840d323e3e9870bea5f8adc877b30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
// Copyright (C) 2017 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.server.project.ProjectCache.noSuchProject;

import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.RetryHelper;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;

/** Manages access control for creating Git references (aka branches, tags). */
@Singleton
public class CreateRefControl {

  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private final PermissionBackend permissionBackend;
  private final ProjectCache projectCache;
  private final Reachable reachable;
  private final RetryHelper retryHelper;

  @Inject
  CreateRefControl(
      PermissionBackend permissionBackend,
      ProjectCache projectCache,
      Reachable reachable,
      RetryHelper retryHelper) {
    this.permissionBackend = permissionBackend;
    this.projectCache = projectCache;
    this.reachable = reachable;
    this.retryHelper = retryHelper;
  }

  /**
   * Checks whether the {@link CurrentUser} can create a new Git ref.
   *
   * @param user the user performing the operation
   * @param repo repository on which user want to create
   * @param destBranch the branch the new {@link RevObject} should be created on
   * @param object the object the user will start the reference with
   * @param sourceBranches the source ref from which the new ref is created from
   * @throws AuthException if creation is denied; the message explains the denial.
   * @throws PermissionBackendException on failure of permission checks.
   * @throws ResourceConflictException if the project state does not permit the operation
   */
  public void checkCreateRef(
      Provider<? extends CurrentUser> user,
      Repository repo,
      BranchNameKey destBranch,
      RevObject object,
      boolean forPush,
      BranchNameKey... sourceBranches)
      throws AuthException, PermissionBackendException, NoSuchProjectException, IOException,
          ResourceConflictException {
    ProjectState ps =
        projectCache.get(destBranch.project()).orElseThrow(noSuchProject(destBranch.project()));
    ps.checkStatePermitsWrite();

    PermissionBackend.ForRef perm = permissionBackend.user(user.get()).ref(destBranch);
    if (object instanceof RevCommit) {
      perm.check(RefPermission.CREATE);
      if (sourceBranches.length == 0) {
        checkCreateCommit(user, repo, (RevCommit) object, ps.getNameKey(), perm, forPush);
      } else {
        for (BranchNameKey src : sourceBranches) {
          PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(src);
          if (forRef.testOrFalse(RefPermission.READ)) {
            return;
          }
        }
        AuthException e =
            new AuthException(
                String.format(
                    "must have %s on existing ref to create new ref from it",
                    RefPermission.READ.describeForException()));
        e.setAdvice(
            String.format(
                "use an existing ref visible to you, or get %s permission on the ref",
                RefPermission.READ.describeForException()));
        throw e;
      }
    } else if (object instanceof RevTag) {
      RevTag tag = (RevTag) object;
      try (RevWalk rw = new RevWalk(repo)) {
        rw.parseBody(tag);
      } catch (IOException e) {
        logger.atSevere().withCause(e).log(
            "RevWalk(%s) parsing %s:", destBranch.project(), tag.name());
        throw e;
      }

      // If tagger is present, require it matches the user's email.
      PersonIdent tagger = tag.getTaggerIdent();
      if (tagger != null
          && (!user.get().isIdentifiedUser()
              || !user.get().asIdentifiedUser().hasEmailAddress(tagger.getEmailAddress()))) {
        perm.check(RefPermission.FORGE_COMMITTER);
      }

      RevObject target = tag.getObject();
      if (target instanceof RevCommit) {
        checkCreateCommit(user, repo, (RevCommit) target, ps.getNameKey(), perm, forPush);
      } else {
        checkCreateRef(user, repo, destBranch, target, forPush);
      }

      // If the tag has a PGP signature, allow a lower level of permission
      // than if it doesn't have a PGP signature.
      PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(destBranch);
      if (tag.getRawGpgSignature() != null) {
        forRef.check(RefPermission.CREATE_SIGNED_TAG);
      } else {
        forRef.check(RefPermission.CREATE_TAG);
      }
    }
  }

  /**
   * Check if the user is allowed to create a new commit object if this creation would introduce a
   * new commit to the repository.
   */
  private void checkCreateCommit(
      Provider<? extends CurrentUser> user,
      Repository repo,
      RevCommit commit,
      Project.NameKey project,
      PermissionBackend.ForRef forRef,
      boolean forPush)
      throws AuthException, PermissionBackendException, IOException {
    try {
      // If the user has UPDATE (push) permission, they can set the ref to an arbitrary commit:
      //
      //  * if they don't have access, we don't advertise the data, and a conforming git client
      //  would send the object along with the push as outcome of the negotation.
      //  * a malicious client could try to send the update without sending the object. This
      //  is prevented by JGit's ConnectivityChecker (see receive.checkReferencedObjectsAreReachable
      //  to switch off this costly check).
      //
      // Thus, when using the git command-line client, we don't need to do extra checks for users
      // with push access.
      //
      // When using the REST API, there is no negotiation, and the target commit must already be on
      // the server, so we must check that the user can see that commit.
      if (forPush) {
        // We can only shortcut for UPDATE permission. Pushing a tag (CREATE_TAG, CREATE_SIGNED_TAG)
        // can also introduce new objects. While there may not be a confidentiality problem
        // (the caller supplies the data as documented above), the permission is for creating
        // tags to existing commits.
        forRef.check(RefPermission.UPDATE);
        return;
      }
    } catch (AuthException denied) {
      // Fall through to check reachability.
    }
    if (reachable.fromRefs(
        project,
        repo,
        commit,
        repo.getRefDatabase().getRefsByPrefix(Constants.R_HEADS, Constants.R_TAGS),
        Optional.of(user.get()))) {
      // If the user has no push permissions, check whether the object is
      // merged into a branch or tag readable by this user. If so, they are
      // not effectively "pushing" more objects, so they can create the ref
      // even if they don't have push permission.
      return;
    }

    // Previous check only catches normal branches. Try PatchSet refs too. If we can create refs,
    // we're not a replica, so we can always use the change index.
    List<ChangeData> changes =
        retryHelper
            .changeIndexQuery(
                "queryChangesByProjectCommitWithLimit1",
                q -> q.enforceVisibility(true).setLimit(1).byProjectCommit(project, commit))
            .call();
    if (!changes.isEmpty()) {
      return;
    }

    AuthException e =
        new AuthException(
            String.format(
                "%s for creating new commit object not permitted",
                RefPermission.UPDATE.describeForException()));
    e.setAdvice(
        String.format(
            "use a SHA1 visible to you, or get %s permission on the ref",
            RefPermission.UPDATE.describeForException()));
    throw e;
  }
}