Line data Source code
1 : // Copyright (C) 2013 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.git.validators;
16 :
17 : import com.google.common.base.Joiner;
18 : import com.google.common.collect.ImmutableList;
19 : import com.google.common.flogger.FluentLogger;
20 : import com.google.gerrit.entities.Account;
21 : import com.google.gerrit.entities.BranchNameKey;
22 : import com.google.gerrit.entities.PatchSet;
23 : import com.google.gerrit.entities.Project;
24 : import com.google.gerrit.entities.RefNames;
25 : import com.google.gerrit.exceptions.StorageException;
26 : import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
27 : import com.google.gerrit.extensions.registration.DynamicMap;
28 : import com.google.gerrit.extensions.registration.Extension;
29 : import com.google.gerrit.extensions.restapi.AuthException;
30 : import com.google.gerrit.server.IdentifiedUser;
31 : import com.google.gerrit.server.account.AccountProperties;
32 : import com.google.gerrit.server.config.AllProjectsName;
33 : import com.google.gerrit.server.config.AllUsersName;
34 : import com.google.gerrit.server.config.GerritServerConfig;
35 : import com.google.gerrit.server.config.PluginConfig;
36 : import com.google.gerrit.server.config.ProjectConfigEntry;
37 : import com.google.gerrit.server.git.CodeReviewCommit;
38 : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
39 : import com.google.gerrit.server.permissions.GlobalPermission;
40 : import com.google.gerrit.server.permissions.PermissionBackend;
41 : import com.google.gerrit.server.permissions.PermissionBackendException;
42 : import com.google.gerrit.server.permissions.ProjectPermission;
43 : import com.google.gerrit.server.plugincontext.PluginSetContext;
44 : import com.google.gerrit.server.project.ProjectCache;
45 : import com.google.gerrit.server.project.ProjectConfig;
46 : import com.google.gerrit.server.project.ProjectState;
47 : import com.google.gerrit.server.query.change.ChangeData;
48 : import com.google.inject.Inject;
49 : import java.io.IOException;
50 : import java.util.List;
51 : import java.util.Objects;
52 : import org.eclipse.jgit.errors.ConfigInvalidException;
53 : import org.eclipse.jgit.lib.Config;
54 : import org.eclipse.jgit.lib.Ref;
55 : import org.eclipse.jgit.lib.Repository;
56 :
57 : /**
58 : * Collection of validators that run inside Gerrit before a change is submitted. The main purpose is
59 : * to ensure that NoteDb data is mutated in a controlled way.
60 : *
61 : * <p>The difference between this and {@link OnSubmitValidators} is that this validates the original
62 : * commit. Depending on the {@link com.google.gerrit.server.submit.SubmitStrategy} that the project
63 : * chooses, the resulting commit in the repo might differ from this original commit. In case you
64 : * want to validate the resulting commit, use {@link OnSubmitValidators}
65 : */
66 : public class MergeValidators {
67 53 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
68 :
69 : private final PluginSetContext<MergeValidationListener> mergeValidationListeners;
70 : private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
71 : private final AccountMergeValidator.Factory accountValidatorFactory;
72 : private final GroupMergeValidator.Factory groupValidatorFactory;
73 :
74 : public interface Factory {
75 : MergeValidators create();
76 : }
77 :
78 : @Inject
79 : MergeValidators(
80 : PluginSetContext<MergeValidationListener> mergeValidationListeners,
81 : ProjectConfigValidator.Factory projectConfigValidatorFactory,
82 : AccountMergeValidator.Factory accountValidatorFactory,
83 53 : GroupMergeValidator.Factory groupValidatorFactory) {
84 53 : this.mergeValidationListeners = mergeValidationListeners;
85 53 : this.projectConfigValidatorFactory = projectConfigValidatorFactory;
86 53 : this.accountValidatorFactory = accountValidatorFactory;
87 53 : this.groupValidatorFactory = groupValidatorFactory;
88 53 : }
89 :
90 : /**
91 : * Runs all validators and throws a {@link MergeValidationException} for the first validator that
92 : * failed. Only the first violation is propagated and processing is stopped thereafter.
93 : */
94 : public void validatePreMerge(
95 : Repository repo,
96 : CodeReviewRevWalk revWalk,
97 : CodeReviewCommit commit,
98 : ProjectState destProject,
99 : BranchNameKey destBranch,
100 : PatchSet.Id patchSetId,
101 : IdentifiedUser caller)
102 : throws MergeValidationException {
103 53 : List<MergeValidationListener> validators =
104 53 : ImmutableList.of(
105 : new PluginMergeValidationListener(mergeValidationListeners),
106 53 : projectConfigValidatorFactory.create(),
107 53 : accountValidatorFactory.create(),
108 53 : groupValidatorFactory.create(),
109 : new DestBranchRefValidator());
110 :
111 53 : for (MergeValidationListener validator : validators) {
112 53 : validator.onPreMerge(repo, revWalk, commit, destProject, destBranch, patchSetId, caller);
113 53 : }
114 53 : }
115 :
116 : /** Validator for any commits to {@code refs/meta/config}. */
117 : public static class ProjectConfigValidator implements MergeValidationListener {
118 : private static final String INVALID_CONFIG =
119 : "Change contains an invalid project configuration.";
120 : private static final String PARENT_NOT_FOUND =
121 : "Change contains an invalid project configuration:\nParent project does not exist.";
122 : private static final String PLUGIN_VALUE_NOT_EDITABLE =
123 : "Change contains an invalid project configuration:\n"
124 : + "One of the plugin configuration parameters is not editable.";
125 : private static final String PLUGIN_VALUE_NOT_PERMITTED =
126 : "Change contains an invalid project configuration:\n"
127 : + "One of the plugin configuration parameters has a value that is not"
128 : + " permitted.";
129 : private static final String ROOT_NO_PARENT =
130 : "Change contains an invalid project configuration:\n"
131 : + "The root project cannot have a parent.";
132 : private static final String SET_BY_ADMIN =
133 : "Change contains a project configuration that changes the parent"
134 : + " project.\n"
135 : + "The change must be submitted by a Gerrit administrator.";
136 : private static final String SET_BY_OWNER =
137 : "Change contains a project configuration that changes the parent"
138 : + " project.\n"
139 : + "The change must be submitted by a Gerrit administrator or the project owner.";
140 :
141 : private final AllProjectsName allProjectsName;
142 : private final AllUsersName allUsersName;
143 : private final ProjectCache projectCache;
144 : private final PermissionBackend permissionBackend;
145 : private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
146 : private final ProjectConfig.Factory projectConfigFactory;
147 : private final boolean allowProjectOwnersToChangeParent;
148 :
149 : public interface Factory {
150 : ProjectConfigValidator create();
151 : }
152 :
153 : @Inject
154 : public ProjectConfigValidator(
155 : AllProjectsName allProjectsName,
156 : AllUsersName allUsersName,
157 : ProjectCache projectCache,
158 : PermissionBackend permissionBackend,
159 : DynamicMap<ProjectConfigEntry> pluginConfigEntries,
160 : ProjectConfig.Factory projectConfigFactory,
161 53 : @GerritServerConfig Config config) {
162 53 : this.allProjectsName = allProjectsName;
163 53 : this.allUsersName = allUsersName;
164 53 : this.projectCache = projectCache;
165 53 : this.permissionBackend = permissionBackend;
166 53 : this.pluginConfigEntries = pluginConfigEntries;
167 53 : this.projectConfigFactory = projectConfigFactory;
168 53 : this.allowProjectOwnersToChangeParent =
169 53 : config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
170 53 : }
171 :
172 : @Override
173 : public void onPreMerge(
174 : Repository repo,
175 : CodeReviewRevWalk revWalk,
176 : CodeReviewCommit commit,
177 : ProjectState destProject,
178 : BranchNameKey destBranch,
179 : PatchSet.Id patchSetId,
180 : IdentifiedUser caller)
181 : throws MergeValidationException {
182 53 : if (RefNames.REFS_CONFIG.equals(destBranch.branch())) {
183 : final Project.NameKey newParent;
184 : try {
185 7 : ProjectConfig cfg = projectConfigFactory.create(destProject.getNameKey());
186 7 : cfg.load(destProject.getNameKey(), repo, commit);
187 7 : newParent = cfg.getProject().getParent(allProjectsName);
188 7 : final Project.NameKey oldParent = destProject.getProject().getParent(allProjectsName);
189 7 : if (oldParent == null) {
190 : // update of the 'All-Projects' project
191 0 : if (newParent != null) {
192 0 : throw new MergeValidationException(ROOT_NO_PARENT);
193 : }
194 : } else {
195 7 : if (!oldParent.equals(newParent)) {
196 1 : if (!allowProjectOwnersToChangeParent) {
197 : try {
198 1 : if (!permissionBackend.user(caller).test(GlobalPermission.ADMINISTRATE_SERVER)) {
199 1 : throw new MergeValidationException(SET_BY_ADMIN);
200 : }
201 0 : } catch (PermissionBackendException e) {
202 0 : logger.atWarning().withCause(e).log("Cannot check ADMINISTRATE_SERVER");
203 0 : throw new MergeValidationException("validation unavailable", e);
204 1 : }
205 : } else {
206 : try {
207 0 : permissionBackend
208 0 : .user(caller)
209 0 : .project(destProject.getNameKey())
210 0 : .check(ProjectPermission.WRITE_CONFIG);
211 0 : } catch (AuthException e) {
212 0 : throw new MergeValidationException(SET_BY_OWNER, e);
213 0 : } catch (PermissionBackendException e) {
214 0 : logger.atWarning().withCause(e).log("Cannot check WRITE_CONFIG");
215 0 : throw new MergeValidationException("validation unavailable", e);
216 0 : }
217 : }
218 1 : if (allUsersName.equals(destProject.getNameKey())
219 0 : && !allProjectsName.equals(newParent)) {
220 0 : throw new MergeValidationException(
221 0 : String.format(
222 0 : " %s must inherit from %s", allUsersName.get(), allProjectsName.get()));
223 : }
224 1 : if (!projectCache.get(newParent).isPresent()) {
225 0 : throw new MergeValidationException(PARENT_NOT_FOUND);
226 : }
227 : }
228 : }
229 :
230 7 : for (Extension<ProjectConfigEntry> e : pluginConfigEntries) {
231 0 : PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName());
232 0 : ProjectConfigEntry configEntry = e.getProvider().get();
233 :
234 0 : String value = pluginCfg.getString(e.getExportName());
235 0 : String oldValue =
236 0 : destProject.getPluginConfig(e.getPluginName()).getString(e.getExportName());
237 :
238 0 : if (!Objects.equals(value, oldValue) && !configEntry.isEditable(destProject)) {
239 0 : throw new MergeValidationException(PLUGIN_VALUE_NOT_EDITABLE);
240 : }
241 :
242 0 : if (ProjectConfigEntryType.LIST.equals(configEntry.getType())
243 : && value != null
244 0 : && !configEntry.getPermittedValues().contains(value)) {
245 0 : throw new MergeValidationException(PLUGIN_VALUE_NOT_PERMITTED);
246 : }
247 0 : }
248 0 : } catch (ConfigInvalidException | IOException e) {
249 0 : throw new MergeValidationException(INVALID_CONFIG, e);
250 7 : }
251 : }
252 53 : }
253 : }
254 :
255 : /** Validator that calls to plugins that provide additional validators. */
256 : public static class PluginMergeValidationListener implements MergeValidationListener {
257 : private final PluginSetContext<MergeValidationListener> mergeValidationListeners;
258 :
259 : public PluginMergeValidationListener(
260 53 : PluginSetContext<MergeValidationListener> mergeValidationListeners) {
261 53 : this.mergeValidationListeners = mergeValidationListeners;
262 53 : }
263 :
264 : @Override
265 : public void onPreMerge(
266 : Repository repo,
267 : CodeReviewRevWalk revWalk,
268 : CodeReviewCommit commit,
269 : ProjectState destProject,
270 : BranchNameKey destBranch,
271 : PatchSet.Id patchSetId,
272 : IdentifiedUser caller)
273 : throws MergeValidationException {
274 53 : mergeValidationListeners.runEach(
275 0 : l -> l.onPreMerge(repo, revWalk, commit, destProject, destBranch, patchSetId, caller),
276 : MergeValidationException.class);
277 53 : }
278 : }
279 :
280 : public static class AccountMergeValidator implements MergeValidationListener {
281 : public interface Factory {
282 : AccountMergeValidator create();
283 : }
284 :
285 : private final AllUsersName allUsersName;
286 : private final ChangeData.Factory changeDataFactory;
287 : private final AccountValidator accountValidator;
288 :
289 : @Inject
290 : public AccountMergeValidator(
291 : AllUsersName allUsersName,
292 : ChangeData.Factory changeDataFactory,
293 53 : AccountValidator accountValidator) {
294 53 : this.allUsersName = allUsersName;
295 53 : this.changeDataFactory = changeDataFactory;
296 53 : this.accountValidator = accountValidator;
297 53 : }
298 :
299 : @Override
300 : public void onPreMerge(
301 : Repository repo,
302 : CodeReviewRevWalk revWalk,
303 : CodeReviewCommit commit,
304 : ProjectState destProject,
305 : BranchNameKey destBranch,
306 : PatchSet.Id patchSetId,
307 : IdentifiedUser caller)
308 : throws MergeValidationException {
309 53 : Account.Id accountId = Account.Id.fromRef(destBranch.branch());
310 53 : if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) {
311 52 : return;
312 : }
313 :
314 1 : ChangeData cd =
315 1 : changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId());
316 : try {
317 1 : if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) {
318 1 : return;
319 : }
320 0 : } catch (StorageException e) {
321 0 : logger.atSevere().withCause(e).log("Cannot validate account update");
322 0 : throw new MergeValidationException("account validation unavailable", e);
323 1 : }
324 :
325 : try {
326 1 : List<String> errorMessages =
327 1 : accountValidator.validate(accountId, repo, revWalk, null, commit);
328 1 : if (!errorMessages.isEmpty()) {
329 1 : throw new MergeValidationException(
330 1 : "invalid account configuration: " + Joiner.on("; ").join(errorMessages));
331 : }
332 0 : } catch (IOException e) {
333 0 : logger.atSevere().withCause(e).log("Cannot validate account update");
334 0 : throw new MergeValidationException("account validation unavailable", e);
335 1 : }
336 1 : }
337 : }
338 :
339 : /** Validator to ensure that group refs are not mutated. */
340 : public static class GroupMergeValidator implements MergeValidationListener {
341 : public interface Factory {
342 : GroupMergeValidator create();
343 : }
344 :
345 : private final AllUsersName allUsersName;
346 :
347 : @Inject
348 53 : public GroupMergeValidator(AllUsersName allUsersName) {
349 53 : this.allUsersName = allUsersName;
350 53 : }
351 :
352 : @Override
353 : public void onPreMerge(
354 : Repository repo,
355 : CodeReviewRevWalk revWalk,
356 : CodeReviewCommit commit,
357 : ProjectState destProject,
358 : BranchNameKey destBranch,
359 : PatchSet.Id patchSetId,
360 : IdentifiedUser caller)
361 : throws MergeValidationException {
362 : // Groups are stored inside the 'All-Users' repository.
363 53 : if (!allUsersName.equals(destProject.getNameKey())
364 2 : || !RefNames.isGroupRef(destBranch.branch())) {
365 53 : return;
366 : }
367 :
368 1 : throw new MergeValidationException("group update not allowed");
369 : }
370 : }
371 :
372 : /**
373 : * Validator to ensure that destBranch is not a symbolic reference (an attempt to merge into a
374 : * symbolic ref branch leads to LOCK_FAILURE exception).
375 : */
376 : private static class DestBranchRefValidator implements MergeValidationListener {
377 : @Override
378 : public void onPreMerge(
379 : Repository repo,
380 : CodeReviewRevWalk revWalk,
381 : CodeReviewCommit commit,
382 : ProjectState destProject,
383 : BranchNameKey destBranch,
384 : PatchSet.Id patchSetId,
385 : IdentifiedUser caller)
386 : throws MergeValidationException {
387 : try {
388 53 : Ref ref = repo.exactRef(destBranch.branch());
389 : // Usually the target branch exists, but there is an exception for some branches (see
390 : // {@link com.google.gerrit.server.git.receive.ReceiveCommits} for details).
391 : // Such non-existing branches should be ignored.
392 53 : if (ref != null && ref.isSymbolic()) {
393 1 : throw new MergeValidationException("the target branch is a symbolic ref");
394 : }
395 0 : } catch (IOException e) {
396 0 : logger.atSevere().withCause(e).log("Cannot validate destination branch");
397 0 : throw new MergeValidationException("symref validation unavailable", e);
398 53 : }
399 53 : }
400 : }
401 : }
|