Line data Source code
1 : // Copyright (C) 2016 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.restapi.project;
16 :
17 : import static com.google.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER;
18 : import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_REF;
19 : import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_TAG_REF;
20 : import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
21 : import static com.google.gerrit.server.permissions.RefPermission.READ;
22 : import static com.google.gerrit.server.permissions.RefPermission.WRITE_CONFIG;
23 : import static com.google.gerrit.server.project.ProjectCache.illegalState;
24 : import static java.util.stream.Collectors.toMap;
25 :
26 : import com.google.common.collect.ImmutableBiMap;
27 : import com.google.common.collect.Iterables;
28 : import com.google.common.flogger.FluentLogger;
29 : import com.google.gerrit.common.Nullable;
30 : import com.google.gerrit.entities.AccessSection;
31 : import com.google.gerrit.entities.AccountGroup;
32 : import com.google.gerrit.entities.GroupDescription;
33 : import com.google.gerrit.entities.Permission;
34 : import com.google.gerrit.entities.PermissionRule;
35 : import com.google.gerrit.entities.Project;
36 : import com.google.gerrit.entities.RefNames;
37 : import com.google.gerrit.extensions.api.access.AccessSectionInfo;
38 : import com.google.gerrit.extensions.api.access.PermissionInfo;
39 : import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
40 : import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
41 : import com.google.gerrit.extensions.common.GroupInfo;
42 : import com.google.gerrit.extensions.restapi.AuthException;
43 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
44 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
45 : import com.google.gerrit.extensions.restapi.Response;
46 : import com.google.gerrit.extensions.restapi.RestReadView;
47 : import com.google.gerrit.server.CurrentUser;
48 : import com.google.gerrit.server.WebLinks;
49 : import com.google.gerrit.server.account.GroupBackend;
50 : import com.google.gerrit.server.config.AllProjectsName;
51 : import com.google.gerrit.server.git.meta.MetaDataUpdate;
52 : import com.google.gerrit.server.permissions.GlobalPermission;
53 : import com.google.gerrit.server.permissions.PermissionBackend;
54 : import com.google.gerrit.server.permissions.PermissionBackendException;
55 : import com.google.gerrit.server.permissions.ProjectPermission;
56 : import com.google.gerrit.server.permissions.RefPermission;
57 : import com.google.gerrit.server.project.ProjectCache;
58 : import com.google.gerrit.server.project.ProjectConfig;
59 : import com.google.gerrit.server.project.ProjectJson;
60 : import com.google.gerrit.server.project.ProjectResource;
61 : import com.google.gerrit.server.project.ProjectState;
62 : import com.google.inject.Inject;
63 : import com.google.inject.Provider;
64 : import com.google.inject.Singleton;
65 : import java.io.IOException;
66 : import java.util.ArrayList;
67 : import java.util.HashMap;
68 : import java.util.HashSet;
69 : import java.util.Map;
70 : import org.eclipse.jgit.errors.ConfigInvalidException;
71 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
72 :
73 : @Singleton
74 : public class GetAccess implements RestReadView<ProjectResource> {
75 146 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
76 :
77 146 : public static final ImmutableBiMap<PermissionRule.Action, PermissionRuleInfo.Action> ACTION_TYPE =
78 146 : ImmutableBiMap.of(
79 : PermissionRule.Action.ALLOW,
80 : PermissionRuleInfo.Action.ALLOW,
81 : PermissionRule.Action.BATCH,
82 : PermissionRuleInfo.Action.BATCH,
83 : PermissionRule.Action.BLOCK,
84 : PermissionRuleInfo.Action.BLOCK,
85 : PermissionRule.Action.DENY,
86 : PermissionRuleInfo.Action.DENY,
87 : PermissionRule.Action.INTERACTIVE,
88 : PermissionRuleInfo.Action.INTERACTIVE);
89 :
90 : private final Provider<CurrentUser> user;
91 : private final PermissionBackend permissionBackend;
92 : private final AllProjectsName allProjectsName;
93 : private final ProjectJson projectJson;
94 : private final ProjectCache projectCache;
95 : private final Provider<MetaDataUpdate.Server> metaDataUpdateFactory;
96 : private final GroupBackend groupBackend;
97 : private final WebLinks webLinks;
98 : private final ProjectConfig.Factory projectConfigFactory;
99 :
100 : @Inject
101 : public GetAccess(
102 : Provider<CurrentUser> self,
103 : PermissionBackend permissionBackend,
104 : AllProjectsName allProjectsName,
105 : ProjectCache projectCache,
106 : Provider<MetaDataUpdate.Server> metaDataUpdateFactory,
107 : ProjectJson projectJson,
108 : GroupBackend groupBackend,
109 : WebLinks webLinks,
110 146 : ProjectConfig.Factory projectConfigFactory) {
111 146 : this.user = self;
112 146 : this.permissionBackend = permissionBackend;
113 146 : this.allProjectsName = allProjectsName;
114 146 : this.projectJson = projectJson;
115 146 : this.projectCache = projectCache;
116 146 : this.metaDataUpdateFactory = metaDataUpdateFactory;
117 146 : this.groupBackend = groupBackend;
118 146 : this.webLinks = webLinks;
119 146 : this.projectConfigFactory = projectConfigFactory;
120 146 : }
121 :
122 : public ProjectAccessInfo apply(Project.NameKey nameKey) throws Exception {
123 9 : ProjectState state =
124 9 : projectCache.get(nameKey).orElseThrow(() -> new ResourceNotFoundException(nameKey.get()));
125 9 : return apply(new ProjectResource(state, user.get())).value();
126 : }
127 :
128 : @Override
129 : public Response<ProjectAccessInfo> apply(ProjectResource rsrc)
130 : throws ResourceNotFoundException, ResourceConflictException, IOException,
131 : PermissionBackendException {
132 : // Load the current configuration from the repository, ensuring it's the most
133 : // recent version available. If it differs from what was in the project
134 : // state, force a cache flush now.
135 :
136 9 : Project.NameKey projectName = rsrc.getNameKey();
137 9 : ProjectAccessInfo info = new ProjectAccessInfo();
138 9 : ProjectState projectState =
139 9 : projectCache.get(projectName).orElseThrow(illegalState(projectName));
140 9 : PermissionBackend.ForProject perm = permissionBackend.currentUser().project(projectName);
141 :
142 : ProjectConfig config;
143 9 : try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
144 9 : config = projectConfigFactory.read(md);
145 9 : info.configWebLinks = new ArrayList<>();
146 :
147 : // config may have a null revision if the repo doesn't have its own refs/meta/config.
148 9 : if (config.getRevision() != null) {
149 9 : info.configWebLinks.addAll(
150 9 : webLinks.getFileHistoryLinks(
151 9 : projectName.get(), config.getRevision().getName(), ProjectConfig.PROJECT_CONFIG));
152 : }
153 :
154 9 : if (config.updateGroupNames(groupBackend)) {
155 0 : md.setMessage("Update group names\n");
156 0 : config.commit(md);
157 0 : projectCache.evictAndReindex(config.getProject());
158 0 : projectState = projectCache.get(projectName).orElseThrow(illegalState(projectName));
159 0 : perm = permissionBackend.currentUser().project(projectName);
160 9 : } else if (config.getRevision() != null
161 9 : && !config.getRevision().equals(projectState.getConfig().getRevision().orElse(null))) {
162 1 : projectCache.evictAndReindex(config.getProject());
163 1 : projectState = projectCache.get(projectName).orElseThrow(illegalState(projectName));
164 1 : perm = permissionBackend.currentUser().project(projectName);
165 : }
166 0 : } catch (ConfigInvalidException e) {
167 0 : throw new ResourceConflictException(e.getMessage());
168 0 : } catch (RepositoryNotFoundException e) {
169 0 : throw new ResourceNotFoundException(rsrc.getName(), e);
170 9 : }
171 :
172 : // The following implementation must match the ProjectAccessFactory JSON RPC endpoint.
173 :
174 9 : info.local = new HashMap<>();
175 9 : info.ownerOf = new HashSet<>();
176 9 : Map<AccountGroup.UUID, GroupInfo> groups = new HashMap<>();
177 9 : boolean canReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
178 9 : boolean canWriteConfig = check(perm, ProjectPermission.WRITE_CONFIG);
179 :
180 : // Check if the project state permits read only when the user is not allowed to write the config
181 : // (=owner). This is so that the owner can still read (and in the next step write) the project's
182 : // config to set the project state to any state that is not HIDDEN.
183 9 : if (!canWriteConfig) {
184 1 : projectState.checkStatePermitsRead();
185 : }
186 :
187 9 : for (AccessSection section : config.getAccessSections()) {
188 7 : String name = section.getName();
189 7 : if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
190 3 : if (canWriteConfig) {
191 3 : info.local.put(name, createAccessSection(groups, section));
192 3 : info.ownerOf.add(name);
193 :
194 0 : } else if (canReadConfig) {
195 0 : info.local.put(section.getName(), createAccessSection(groups, section));
196 : }
197 :
198 7 : } else if (AccessSection.isValidRefSectionName(name)) {
199 7 : if (check(perm, name, WRITE_CONFIG)) {
200 7 : info.local.put(name, createAccessSection(groups, section));
201 7 : info.ownerOf.add(name);
202 :
203 1 : } else if (canReadConfig) {
204 0 : info.local.put(name, createAccessSection(groups, section));
205 :
206 1 : } else if (check(perm, name, READ)) {
207 : // Filter the section to only add rules describing groups that
208 : // are visible to the current-user. This includes any group the
209 : // user is a member of, as well as groups they own or that
210 : // are visible to all users.
211 :
212 1 : AccessSection.Builder dst = null;
213 1 : for (Permission srcPerm : section.getPermissions()) {
214 1 : Permission.Builder dstPerm = null;
215 :
216 1 : for (PermissionRule srcRule : srcPerm.getRules()) {
217 1 : AccountGroup.UUID groupId = srcRule.getGroup().getUUID();
218 1 : if (groupId == null) {
219 0 : continue;
220 : }
221 :
222 1 : loadGroup(groups, groupId);
223 1 : if (dstPerm == null) {
224 1 : if (dst == null) {
225 1 : dst = AccessSection.builder(name);
226 1 : info.local.put(name, createAccessSection(groups, dst.build()));
227 : }
228 1 : dstPerm = dst.upsertPermission(srcPerm.getName());
229 : }
230 1 : dstPerm.add(srcRule.toBuilder());
231 1 : }
232 1 : }
233 : }
234 : }
235 7 : }
236 :
237 9 : if (info.ownerOf.isEmpty()) {
238 : try {
239 3 : permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
240 : // Special case: If the section list is empty, this project has no current
241 : // access control information. Fall back to site administrators.
242 3 : info.ownerOf.add(AccessSection.ALL);
243 1 : } catch (AuthException e) {
244 : // Do nothing.
245 3 : }
246 : }
247 :
248 9 : if (config.getRevision() != null) {
249 9 : info.revision = config.getRevision().name();
250 : }
251 :
252 9 : ProjectState parent = Iterables.getFirst(projectState.parents(), null);
253 9 : if (parent != null) {
254 7 : info.inheritsFrom = projectJson.format(parent.getProject());
255 : }
256 :
257 9 : if (projectName.equals(allProjectsName)
258 3 : && permissionBackend.currentUser().testOrFalse(ADMINISTRATE_SERVER)) {
259 3 : info.ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
260 : }
261 :
262 9 : info.isOwner = toBoolean(canWriteConfig);
263 9 : info.canUpload =
264 9 : toBoolean(
265 9 : projectState.statePermitsWrite()
266 : && (canWriteConfig
267 : || (canReadConfig
268 9 : && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE))));
269 9 : info.canAdd = toBoolean(perm.testOrFalse(CREATE_REF));
270 9 : info.canAddTags = toBoolean(perm.testOrFalse(CREATE_TAG_REF));
271 9 : info.configVisible = canReadConfig || canWriteConfig;
272 :
273 9 : info.groups =
274 9 : groups.entrySet().stream()
275 9 : .filter(e -> e.getValue() != null)
276 9 : .collect(toMap(e -> e.getKey().get(), Map.Entry::getValue));
277 :
278 9 : return Response.ok(info);
279 : }
280 :
281 : private void loadGroup(Map<AccountGroup.UUID, GroupInfo> groups, AccountGroup.UUID id) {
282 7 : if (!groups.containsKey(id)) {
283 7 : GroupDescription.Basic basic = groupBackend.get(id);
284 : GroupInfo group;
285 7 : if (basic != null) {
286 7 : group = new GroupInfo();
287 : // The UI only needs name + URL, so don't populate other fields to avoid leaking data
288 : // about groups invisible to the user.
289 7 : group.name = basic.getName();
290 7 : group.url = basic.getUrl();
291 : } else {
292 0 : logger.atWarning().log("no such group: %s", id);
293 0 : group = null;
294 : }
295 7 : groups.put(id, group);
296 : }
297 7 : }
298 :
299 : private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm)
300 : throws PermissionBackendException {
301 : try {
302 9 : ctx.ref(ref).check(perm);
303 9 : return true;
304 3 : } catch (AuthException denied) {
305 3 : return false;
306 : }
307 : }
308 :
309 : private static boolean check(PermissionBackend.ForProject ctx, ProjectPermission perm)
310 : throws PermissionBackendException {
311 : try {
312 9 : ctx.check(perm);
313 9 : return true;
314 1 : } catch (AuthException denied) {
315 1 : return false;
316 : }
317 : }
318 :
319 : private AccessSectionInfo createAccessSection(
320 : Map<AccountGroup.UUID, GroupInfo> groups, AccessSection section) {
321 7 : AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
322 7 : accessSectionInfo.permissions = new HashMap<>();
323 7 : for (Permission p : section.getPermissions()) {
324 7 : PermissionInfo pInfo = new PermissionInfo(p.getLabel(), p.getExclusiveGroup() ? true : null);
325 7 : pInfo.rules = new HashMap<>();
326 7 : for (PermissionRule r : p.getRules()) {
327 7 : PermissionRuleInfo info =
328 7 : new PermissionRuleInfo(ACTION_TYPE.get(r.getAction()), r.getForce());
329 7 : if (r.hasRange()) {
330 3 : info.max = r.getMax();
331 3 : info.min = r.getMin();
332 : }
333 7 : AccountGroup.UUID group = r.getGroup().getUUID();
334 7 : if (group != null) {
335 7 : pInfo.rules.putIfAbsent(group.get(), info); // First entry for the group wins
336 7 : loadGroup(groups, group);
337 : }
338 7 : }
339 7 : accessSectionInfo.permissions.put(p.getName(), pInfo);
340 7 : }
341 7 : return accessSectionInfo;
342 : }
343 :
344 : @Nullable
345 : private static Boolean toBoolean(boolean value) {
346 9 : return value ? true : null;
347 : }
348 : }
|