1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.metadata;
18
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation.asJsonArray;
21 import static com.linecorp.centraldogma.internal.jsonpatch.JsonPatchUtil.encodeSegment;
22 import static com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager.listProjectsWithoutDogma;
23 import static com.linecorp.centraldogma.server.internal.storage.project.ProjectInitializer.INTERNAL_PROJECT_DOGMA;
24 import static com.linecorp.centraldogma.server.metadata.RepositorySupport.convertWithJackson;
25 import static com.linecorp.centraldogma.server.metadata.Tokens.SECRET_PREFIX;
26 import static com.linecorp.centraldogma.server.metadata.Tokens.validateSecret;
27 import static java.util.Objects.requireNonNull;
28
29 import java.util.Collection;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.UUID;
33 import java.util.concurrent.CompletableFuture;
34 import java.util.concurrent.ConcurrentHashMap;
35
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import com.fasterxml.jackson.core.JsonPointer;
40 import com.fasterxml.jackson.databind.JsonNode;
41 import com.google.common.collect.ImmutableList;
42 import com.spotify.futures.CompletableFutures;
43
44 import com.linecorp.armeria.common.util.Exceptions;
45 import com.linecorp.centraldogma.common.Author;
46 import com.linecorp.centraldogma.common.Change;
47 import com.linecorp.centraldogma.common.ChangeConflictException;
48 import com.linecorp.centraldogma.common.RepositoryExistsException;
49 import com.linecorp.centraldogma.common.Revision;
50 import com.linecorp.centraldogma.internal.Jackson;
51 import com.linecorp.centraldogma.internal.jsonpatch.AddOperation;
52 import com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation;
53 import com.linecorp.centraldogma.internal.jsonpatch.RemoveIfExistsOperation;
54 import com.linecorp.centraldogma.internal.jsonpatch.RemoveOperation;
55 import com.linecorp.centraldogma.internal.jsonpatch.ReplaceOperation;
56 import com.linecorp.centraldogma.internal.jsonpatch.TestAbsenceOperation;
57 import com.linecorp.centraldogma.server.QuotaConfig;
58 import com.linecorp.centraldogma.server.command.CommandExecutor;
59 import com.linecorp.centraldogma.server.storage.project.Project;
60 import com.linecorp.centraldogma.server.storage.project.ProjectManager;
61
62
63
64
65 public class MetadataService {
66
67 private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
68
69
70
71
72 public static final String METADATA_JSON = "/metadata.json";
73
74
75
76
77 public static final String TOKEN_JSON = "/tokens.json";
78
79
80
81
82 private static final JsonPointer PROJECT_REMOVAL = JsonPointer.compile("/removal");
83
84 private final ProjectManager projectManager;
85 private final RepositorySupport<ProjectMetadata> metadataRepo;
86 private final RepositorySupport<Tokens> tokenRepo;
87 private final CommandExecutor executor;
88
89 private final Map<String, CompletableFuture<Revision>> reposInAddingMetadata = new ConcurrentHashMap<>();
90
91
92
93
94 public MetadataService(ProjectManager projectManager, CommandExecutor executor) {
95 this.projectManager = requireNonNull(projectManager, "projectManager");
96 this.executor = requireNonNull(executor, "executor");
97 metadataRepo = new RepositorySupport<>(projectManager, executor,
98 entry -> convertWithJackson(entry, ProjectMetadata.class));
99 tokenRepo = new RepositorySupport<>(projectManager, executor,
100 entry -> convertWithJackson(entry, Tokens.class));
101 }
102
103
104
105
106 public CompletableFuture<ProjectMetadata> getProject(String projectName) {
107 requireNonNull(projectName, "projectName");
108 return fetchMetadata(projectName).thenApply(HolderWithRevision::object);
109 }
110
111 private CompletableFuture<HolderWithRevision<ProjectMetadata>> fetchMetadata(String projectName) {
112 return fetchMetadata0(projectName).thenCompose(holder -> {
113 final Set<String> repos = projectManager.get(projectName).repos().list().keySet();
114 final Set<String> reposWithMetadata = holder.object().repos().keySet();
115
116
117
118
119 final ImmutableList.Builder<CompletableFuture<Revision>> builder = ImmutableList.builder();
120 for (String repo : repos) {
121 if (reposWithMetadata.contains(repo) ||
122 repo.equals(Project.REPO_DOGMA)) {
123 continue;
124 }
125
126 final String projectAndRepositoryName = projectName + '/' + repo;
127 final CompletableFuture<Revision> future = new CompletableFuture<>();
128 final CompletableFuture<Revision> futureInMap =
129 reposInAddingMetadata.computeIfAbsent(projectAndRepositoryName, key -> future);
130 if (futureInMap != future) {
131 builder.add(futureInMap);
132 continue;
133 }
134
135 logger.warn("Adding missing repository metadata: {}/{}", projectName, repo);
136 final Author author = projectManager.get(projectName).repos().get(repo).author();
137 final CompletableFuture<Revision> addRepoFuture = addRepo(author, projectName, repo);
138 addRepoFuture.handle((revision, cause) -> {
139 if (cause != null) {
140 future.completeExceptionally(cause);
141 } else {
142 future.complete(revision);
143 }
144 reposInAddingMetadata.remove(projectAndRepositoryName);
145 return null;
146 });
147 builder.add(future);
148 }
149
150 final ImmutableList<CompletableFuture<Revision>> futures = builder.build();
151 if (futures.isEmpty()) {
152
153 return CompletableFuture.completedFuture(holder);
154 }
155
156
157 return CompletableFutures.successfulAsList(futures, cause -> {
158 final Throwable peeled = Exceptions.peel(cause);
159
160 if (peeled instanceof RepositoryExistsException) {
161 return null;
162 }
163 return Exceptions.throwUnsafely(cause);
164 }).thenCompose(unused -> {
165 logger.info("Fetching {}/{}{} again",
166 projectName, Project.REPO_DOGMA, METADATA_JSON);
167 return fetchMetadata0(projectName);
168 });
169 });
170 }
171
172 private CompletableFuture<HolderWithRevision<ProjectMetadata>> fetchMetadata0(String projectName) {
173 return metadataRepo.fetch(projectName, Project.REPO_DOGMA, METADATA_JSON);
174 }
175
176
177
178
179 public CompletableFuture<Revision> removeProject(Author author, String projectName) {
180 requireNonNull(author, "author");
181 requireNonNull(projectName, "projectName");
182
183 final Change<JsonNode> change = Change.ofJsonPatch(
184 METADATA_JSON,
185 asJsonArray(new TestAbsenceOperation(PROJECT_REMOVAL),
186 new AddOperation(PROJECT_REMOVAL,
187 Jackson.valueToTree(UserAndTimestamp.of(author)))));
188 return metadataRepo.push(projectName, Project.REPO_DOGMA, author,
189 "Remove the project: " + projectName, change);
190 }
191
192
193
194
195 public CompletableFuture<Revision> restoreProject(Author author, String projectName) {
196 requireNonNull(author, "author");
197 requireNonNull(projectName, "projectName");
198
199 final Change<JsonNode> change =
200 Change.ofJsonPatch(METADATA_JSON, new RemoveOperation(PROJECT_REMOVAL).toJsonNode());
201 return metadataRepo.push(projectName, Project.REPO_DOGMA, author,
202 "Restore the project: " + projectName, change);
203 }
204
205
206
207
208 public CompletableFuture<Member> getMember(String projectName, User user) {
209 requireNonNull(projectName, "projectName");
210 requireNonNull(user, "user");
211
212 return getProject(projectName).thenApply(
213 project -> project.memberOrDefault(user.id(), null));
214 }
215
216
217
218
219
220 public CompletableFuture<Revision> addMember(Author author, String projectName,
221 User member, ProjectRole projectRole) {
222 requireNonNull(author, "author");
223 requireNonNull(projectName, "projectName");
224 requireNonNull(member, "member");
225 requireNonNull(projectRole, "projectRole");
226
227 final Member newMember = new Member(member, projectRole, UserAndTimestamp.of(author));
228 final JsonPointer path = JsonPointer.compile("/members" + encodeSegment(newMember.id()));
229 final Change<JsonNode> change =
230 Change.ofJsonPatch(METADATA_JSON,
231 asJsonArray(new TestAbsenceOperation(path),
232 new AddOperation(path, Jackson.valueToTree(newMember))));
233 final String commitSummary =
234 "Add a member '" + newMember.id() + "' to the project '" + projectName + '\'';
235 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
236 }
237
238
239
240
241
242
243 public CompletableFuture<Revision> removeMember(Author author, String projectName, User member) {
244 requireNonNull(author, "author");
245 requireNonNull(projectName, "projectName");
246 requireNonNull(member, "member");
247
248 final String commitSummary =
249 "Remove the member '" + member.id() + "' from the project '" + projectName + '\'';
250 return metadataRepo.push(
251 projectName, Project.REPO_DOGMA, author, commitSummary,
252 () -> fetchMetadata(projectName).thenApply(
253 metadataWithRevision -> {
254 final ImmutableList.Builder<JsonPatchOperation> patches = ImmutableList.builder();
255 metadataWithRevision
256 .object().repos().values()
257 .stream().filter(r -> r.perUserPermissions().containsKey(member.id()))
258 .forEach(r -> patches.add(new RemoveOperation(
259 perUserPermissionPointer(r.name(), member.id()))));
260 patches.add(new RemoveOperation(JsonPointer.compile("/members" +
261 encodeSegment(member.id()))));
262 final Change<JsonNode> change =
263 Change.ofJsonPatch(METADATA_JSON, Jackson.valueToTree(patches.build()));
264 return HolderWithRevision.of(change, metadataWithRevision.revision());
265 })
266 );
267 }
268
269
270
271
272 public CompletableFuture<Revision> updateMemberRole(Author author, String projectName,
273 User member, ProjectRole projectRole) {
274 requireNonNull(author, "author");
275 requireNonNull(projectName, "projectName");
276 requireNonNull(member, "member");
277 requireNonNull(projectRole, "projectRole");
278
279 final Change<JsonNode> change = Change.ofJsonPatch(
280 METADATA_JSON,
281 new ReplaceOperation(JsonPointer.compile("/members" + encodeSegment(member.id()) + "/role"),
282 Jackson.valueToTree(projectRole)).toJsonNode());
283 final String commitSummary = "Updates the role of the member '" + member.id() +
284 "' as '" + projectRole + "' for the project '" + projectName + '\'';
285 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
286 }
287
288
289
290
291
292 public CompletableFuture<RepositoryMetadata> getRepo(String projectName, String repoName) {
293 requireNonNull(projectName, "projectName");
294 requireNonNull(repoName, "repoName");
295
296 return getProject(projectName).thenApply(project -> project.repo(repoName));
297 }
298
299
300
301
302
303 public CompletableFuture<Revision> addRepo(Author author, String projectName, String repoName) {
304 return addRepo(author, projectName, repoName, PerRolePermissions.ofDefault());
305 }
306
307
308
309
310
311 public CompletableFuture<Revision> addRepo(Author author, String projectName, String repoName,
312 PerRolePermissions permission) {
313 requireNonNull(author, "author");
314 requireNonNull(projectName, "projectName");
315 requireNonNull(repoName, "repoName");
316 requireNonNull(permission, "permission");
317
318 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName));
319 final RepositoryMetadata newRepositoryMetadata = new RepositoryMetadata(repoName,
320 UserAndTimestamp.of(author),
321 permission);
322 final Change<JsonNode> change =
323 Change.ofJsonPatch(METADATA_JSON,
324 asJsonArray(new TestAbsenceOperation(path),
325 new AddOperation(path,
326 Jackson.valueToTree(newRepositoryMetadata))));
327 final String commitSummary =
328 "Add a repo '" + newRepositoryMetadata.id() + "' to the project '" + projectName + '\'';
329 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change)
330 .handle((revision, cause) -> {
331 if (cause != null) {
332 if (Exceptions.peel(cause) instanceof ChangeConflictException) {
333 throw new RepositoryExistsException(repoName);
334 } else {
335 return Exceptions.throwUnsafely(cause);
336 }
337 }
338 return revision;
339 });
340 }
341
342
343
344
345
346 public CompletableFuture<Revision> removeRepo(Author author, String projectName, String repoName) {
347 requireNonNull(author, "author");
348 requireNonNull(projectName, "projectName");
349 requireNonNull(repoName, "repoName");
350
351 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) + "/removal");
352 final Change<JsonNode> change =
353 Change.ofJsonPatch(METADATA_JSON,
354 asJsonArray(new TestAbsenceOperation(path),
355 new AddOperation(path, Jackson.valueToTree(
356 UserAndTimestamp.of(author)))));
357 final String commitSummary =
358 "Remove the repo '" + repoName + "' from the project '" + projectName + '\'';
359 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
360 }
361
362
363
364
365
366 public CompletableFuture<Revision> purgeRepo(Author author, String projectName, String repoName) {
367 requireNonNull(author, "author");
368 requireNonNull(projectName, "projectName");
369 requireNonNull(repoName, "repoName");
370
371 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName));
372 final Change<JsonNode> change = Change.ofJsonPatch(METADATA_JSON,
373 new RemoveOperation(path).toJsonNode());
374 final String commitSummary =
375 "Purge the repo '" + repoName + "' from the project '" + projectName + '\'';
376 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
377 }
378
379
380
381
382
383 public CompletableFuture<Revision> restoreRepo(Author author, String projectName, String repoName) {
384 requireNonNull(author, "author");
385 requireNonNull(projectName, "projectName");
386 requireNonNull(repoName, "repoName");
387
388 final Change<JsonNode> change =
389 Change.ofJsonPatch(METADATA_JSON,
390 new RemoveOperation(JsonPointer.compile(
391 "/repos" + encodeSegment(repoName) + "/removal")).toJsonNode());
392 final String commitSummary =
393 "Restore the repo '" + repoName + "' from the project '" + projectName + '\'';
394 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
395 }
396
397
398
399
400
401 public CompletableFuture<Revision> updatePerRolePermissions(Author author,
402 String projectName, String repoName,
403 PerRolePermissions perRolePermissions) {
404 requireNonNull(author, "author");
405 requireNonNull(projectName, "projectName");
406 requireNonNull(repoName, "repoName");
407 requireNonNull(perRolePermissions, "perRolePermissions");
408
409 if (Project.REPO_DOGMA.equals(repoName)) {
410 throw new UnsupportedOperationException(
411 "Can't update the per role permission for internal repository: " + repoName);
412 }
413
414 final Set<Permission> anonymous = perRolePermissions.anonymous();
415 if (Project.REPO_META.equals(repoName)) {
416 final Set<Permission> guest = perRolePermissions.guest();
417 if (!guest.isEmpty() || !anonymous.isEmpty()) {
418 throw new UnsupportedOperationException(
419 "Can't give a permission to guest or anonymous for internal repository: " + repoName);
420 }
421 }
422 if (anonymous.contains(Permission.WRITE)) {
423 throw new IllegalArgumentException("Anonymous users cannot have write permission.");
424 }
425
426 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) +
427 "/perRolePermissions");
428 final Change<JsonNode> change =
429 Change.ofJsonPatch(METADATA_JSON,
430 new ReplaceOperation(path, Jackson.valueToTree(perRolePermissions))
431 .toJsonNode());
432 final String commitSummary =
433 "Update the role permission of the '" + repoName + "' in the project '" + projectName + '\'';
434 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
435 }
436
437
438
439
440 public CompletableFuture<Revision> addToken(Author author, String projectName,
441 Token token, ProjectRole role) {
442 return addToken(author, projectName, requireNonNull(token, "token").appId(), role);
443 }
444
445
446
447
448 public CompletableFuture<Revision> addToken(Author author, String projectName,
449 String appId, ProjectRole role) {
450 requireNonNull(author, "author");
451 requireNonNull(projectName, "projectName");
452 requireNonNull(appId, "appId");
453 requireNonNull(role, "role");
454
455 return getTokens().thenCompose(tokens -> {
456 final Token token = tokens.appIds().get(appId);
457 checkArgument(token != null, "Token not found: " + appId);
458
459 final TokenRegistration registration = new TokenRegistration(appId, role,
460 UserAndTimestamp.of(author));
461 final JsonPointer path = JsonPointer.compile("/tokens" + encodeSegment(registration.id()));
462 final Change<JsonNode> change =
463 Change.ofJsonPatch(METADATA_JSON,
464 asJsonArray(new TestAbsenceOperation(path),
465 new AddOperation(path, Jackson.valueToTree(registration))));
466 final String commitSummary = "Add a token '" + registration.id() +
467 "' to the project '" + projectName + "' with a role '" + role + '\'';
468 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
469 });
470 }
471
472
473
474
475
476 public CompletableFuture<Revision> removeToken(Author author, String projectName, Token token) {
477 return removeToken(author, projectName, requireNonNull(token, "token").appId());
478 }
479
480
481
482
483
484 public CompletableFuture<Revision> removeToken(Author author, String projectName, String appId) {
485 requireNonNull(author, "author");
486 requireNonNull(projectName, "projectName");
487 requireNonNull(appId, "appId");
488
489 return removeToken(projectName, author, appId, false);
490 }
491
492 private CompletableFuture<Revision> removeToken(String projectName, Author author, String appId,
493 boolean quiet) {
494 final String commitSummary = "Remove the token '" + appId + "' from the project '" + projectName + '\'';
495 return metadataRepo.push(
496 projectName, Project.REPO_DOGMA, author, commitSummary,
497 () -> fetchMetadata(projectName).thenApply(metadataWithRevision -> {
498 final ImmutableList.Builder<JsonPatchOperation> patches = ImmutableList.builder();
499 final ProjectMetadata metadata = metadataWithRevision.object();
500 metadata.repos().values()
501 .stream().filter(repo -> repo.perTokenPermissions().containsKey(appId))
502 .forEach(r -> patches.add(
503 new RemoveOperation(perTokenPermissionPointer(r.name(), appId))));
504 if (quiet) {
505 patches.add(new RemoveIfExistsOperation(JsonPointer.compile("/tokens" +
506 encodeSegment(appId))));
507 } else {
508 patches.add(new RemoveOperation(JsonPointer.compile("/tokens" +
509 encodeSegment(appId))));
510 }
511 final Change<JsonNode> change =
512 Change.ofJsonPatch(METADATA_JSON, Jackson.valueToTree(patches.build()));
513 return HolderWithRevision.of(change, metadataWithRevision.revision());
514 })
515 );
516 }
517
518
519
520
521 public CompletableFuture<Revision> updateTokenRole(Author author, String projectName,
522 Token token, ProjectRole role) {
523 requireNonNull(author, "author");
524 requireNonNull(projectName, "projectName");
525 requireNonNull(token, "token");
526 requireNonNull(role, "role");
527
528 final TokenRegistration registration = new TokenRegistration(token.appId(), role,
529 UserAndTimestamp.of(author));
530 final JsonPointer path = JsonPointer.compile("/tokens" + encodeSegment(registration.id()));
531 final Change<JsonNode> change =
532 Change.ofJsonPatch(METADATA_JSON,
533 new ReplaceOperation(path, Jackson.valueToTree(registration))
534 .toJsonNode());
535 final String commitSummary = "Update the role of a token '" + token.appId() +
536 "' as '" + role + "' for the project '" + projectName + '\'';
537 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
538 }
539
540
541
542
543
544 public CompletableFuture<Revision> addPerUserPermission(Author author, String projectName,
545 String repoName, User member,
546 Collection<Permission> permission) {
547 requireNonNull(author, "author");
548 requireNonNull(projectName, "projectName");
549 requireNonNull(repoName, "repoName");
550 requireNonNull(member, "member");
551 requireNonNull(permission, "permission");
552
553 return getProject(projectName).thenCompose(project -> {
554 ensureProjectMember(project, member);
555 final String commitSummary = "Add permission of '" + member.id() +
556 "' as '" + permission + "' to the project '" + projectName + '\n';
557 return addPermissionAtPointer(author, projectName, perUserPermissionPointer(repoName, member.id()),
558 permission, commitSummary);
559 });
560 }
561
562
563
564
565
566 public CompletableFuture<Revision> removePerUserPermission(Author author, String projectName,
567 String repoName, User member) {
568 requireNonNull(author, "author");
569 requireNonNull(projectName, "projectName");
570 requireNonNull(repoName, "repoName");
571 requireNonNull(member, "member");
572
573 final String memberId = member.id();
574 return removePermissionAtPointer(author, projectName,
575 perUserPermissionPointer(repoName, memberId),
576 "Remove permission of the '" + memberId +
577 "' from the project '" + projectName + '\'');
578 }
579
580
581
582
583
584 public CompletableFuture<Revision> updatePerUserPermission(Author author, String projectName,
585 String repoName, User member,
586 Collection<Permission> permission) {
587 requireNonNull(author, "author");
588 requireNonNull(projectName, "projectName");
589 requireNonNull(repoName, "repoName");
590 requireNonNull(member, "member");
591 requireNonNull(permission, "permission");
592
593 final String memberId = member.id();
594 return replacePermissionAtPointer(author, projectName,
595 perUserPermissionPointer(repoName, memberId), permission,
596 "Update permission of the '" + memberId +
597 "' as '" + permission + "' for the project '" + projectName + '\'');
598 }
599
600
601
602
603
604 public CompletableFuture<Revision> addPerTokenPermission(Author author, String projectName,
605 String repoName, String appId,
606 Collection<Permission> permission) {
607 requireNonNull(author, "author");
608 requireNonNull(projectName, "projectName");
609 requireNonNull(repoName, "repoName");
610 requireNonNull(appId, "appId");
611 requireNonNull(permission, "permission");
612
613 return getProject(projectName).thenCompose(project -> {
614 ensureProjectToken(project, appId);
615 return addPermissionAtPointer(author, projectName,
616 perTokenPermissionPointer(repoName, appId), permission,
617 "Add permission of the token '" + appId +
618 "' as '" + permission + "' to the project '" + projectName + '\'');
619 });
620 }
621
622
623
624
625
626 public CompletableFuture<Revision> removePerTokenPermission(Author author, String projectName,
627 String repoName, String appId) {
628 requireNonNull(author, "author");
629 requireNonNull(projectName, "projectName");
630 requireNonNull(repoName, "repoName");
631 requireNonNull(appId, "appId");
632
633 return removePermissionAtPointer(author, projectName,
634 perTokenPermissionPointer(repoName, appId),
635 "Remove permission of the token '" + appId +
636 "' from the project '" + projectName + '\'');
637 }
638
639
640
641
642
643 public CompletableFuture<Revision> updatePerTokenPermission(Author author, String projectName,
644 String repoName, String appId,
645 Collection<Permission> permission) {
646 requireNonNull(author, "author");
647 requireNonNull(projectName, "projectName");
648 requireNonNull(repoName, "repoName");
649 requireNonNull(appId, "appId");
650 requireNonNull(permission, "permission");
651
652 return replacePermissionAtPointer(author, projectName,
653 perTokenPermissionPointer(repoName, appId), permission,
654 "Update permission of the token '" + appId +
655 "' as '" + permission + "' for the project '" + projectName + '\'');
656 }
657
658
659
660
661
662 public CompletableFuture<Revision> updateWriteQuota(
663 Author author, String projectName, String repoName, QuotaConfig writeQuota) {
664 requireNonNull(author, "author");
665 requireNonNull(projectName, "projectName");
666 requireNonNull(repoName, "repoName");
667 requireNonNull(writeQuota, "writeQuota");
668
669 final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) + "/writeQuota");
670 final Change<JsonNode> change =
671 Change.ofJsonPatch(METADATA_JSON,
672 new AddOperation(path, Jackson.valueToTree(writeQuota)).toJsonNode());
673 final String commitSummary = "Update a write quota for the repository '" + repoName + '\'';
674 executor.setWriteQuota(projectName, repoName, writeQuota);
675 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
676 }
677
678
679
680
681 private CompletableFuture<Revision> addPermissionAtPointer(Author author,
682 String projectName, JsonPointer path,
683 Collection<Permission> permission,
684 String commitSummary) {
685 final Change<JsonNode> change =
686 Change.ofJsonPatch(METADATA_JSON,
687 asJsonArray(new TestAbsenceOperation(path),
688 new AddOperation(path, Jackson.valueToTree(permission))));
689 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
690 }
691
692
693
694
695 private CompletableFuture<Revision> removePermissionAtPointer(Author author, String projectName,
696 JsonPointer path, String commitSummary) {
697 final Change<JsonNode> change = Change.ofJsonPatch(METADATA_JSON,
698 new RemoveOperation(path).toJsonNode());
699 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
700 }
701
702
703
704
705 private CompletableFuture<Revision> replacePermissionAtPointer(Author author,
706 String projectName, JsonPointer path,
707 Collection<Permission> permission,
708 String commitSummary) {
709 final Change<JsonNode> change =
710 Change.ofJsonPatch(METADATA_JSON,
711 new ReplaceOperation(path, Jackson.valueToTree(permission)).toJsonNode());
712 return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
713 }
714
715
716
717
718
719 public CompletableFuture<Collection<Permission>> findPermissions(String projectName, String repoName,
720 User user) {
721 requireNonNull(user, "user");
722 if (user.isAdmin()) {
723 return CompletableFuture.completedFuture(PerRolePermissions.ALL_PERMISSION);
724 }
725 if (user instanceof UserWithToken) {
726 return findPermissions(projectName, repoName, ((UserWithToken) user).token().appId());
727 } else {
728 return findPermissions0(projectName, repoName, user);
729 }
730 }
731
732
733
734
735
736 public CompletableFuture<Collection<Permission>> findPermissions(String projectName, String repoName,
737 String appId) {
738 requireNonNull(projectName, "projectName");
739 requireNonNull(repoName, "repoName");
740 requireNonNull(appId, "appId");
741
742 return getProject(projectName).thenApply(metadata -> {
743 final RepositoryMetadata repositoryMetadata = metadata.repo(repoName);
744 final TokenRegistration registration = metadata.tokens().getOrDefault(appId, null);
745
746
747 if (registration == null) {
748 return repositoryMetadata.perRolePermissions().guest();
749 }
750 final Collection<Permission> p = repositoryMetadata.perTokenPermissions().get(registration.id());
751 if (p != null) {
752 return p;
753 }
754 return findPerRolePermissions(repositoryMetadata, registration.role());
755 });
756 }
757
758
759
760
761
762 private CompletableFuture<Collection<Permission>> findPermissions0(String projectName, String repoName,
763 User user) {
764 requireNonNull(projectName, "projectName");
765 requireNonNull(repoName, "repoName");
766 requireNonNull(user, "user");
767
768 return getProject(projectName).thenApply(metadata -> {
769 final RepositoryMetadata repositoryMetadata = metadata.repo(repoName);
770 final Member member = metadata.memberOrDefault(user.id(), null);
771
772
773 if (member == null) {
774 return !user.isAnonymous() ? repositoryMetadata.perRolePermissions().guest()
775 : repositoryMetadata.perRolePermissions().anonymous();
776 }
777 final Collection<Permission> p = repositoryMetadata.perUserPermissions().get(member.id());
778 if (p != null) {
779 return p;
780 }
781 return findPerRolePermissions(repositoryMetadata, member.role());
782 });
783 }
784
785 private static Collection<Permission> findPerRolePermissions(RepositoryMetadata repositoryMetadata,
786 ProjectRole role) {
787 switch (role) {
788 case OWNER:
789 return repositoryMetadata.perRolePermissions().owner();
790 case MEMBER:
791 return repositoryMetadata.perRolePermissions().member();
792 case GUEST:
793 return repositoryMetadata.perRolePermissions().guest();
794 default:
795 return repositoryMetadata.perRolePermissions().anonymous();
796 }
797 }
798
799
800
801
802 public CompletableFuture<ProjectRole> findRole(String projectName, User user) {
803 requireNonNull(projectName, "projectName");
804 requireNonNull(user, "user");
805
806 if (user.isAdmin()) {
807 return CompletableFuture.completedFuture(ProjectRole.OWNER);
808 }
809 return getProject(projectName).thenApply(project -> {
810 if (user instanceof UserWithToken) {
811 final TokenRegistration registration = project.tokens().getOrDefault(
812 ((UserWithToken) user).token().id(), null);
813 return registration != null ? registration.role() : ProjectRole.GUEST;
814 } else {
815 final Member member = project.memberOrDefault(user.id(), null);
816 return member != null ? member.role() : ProjectRole.GUEST;
817 }
818 });
819 }
820
821
822
823
824 public CompletableFuture<Tokens> getTokens() {
825 return tokenRepo.fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
826 .thenApply(HolderWithRevision::object);
827 }
828
829
830
831
832
833 public CompletableFuture<Revision> createToken(Author author, String appId) {
834 return createToken(author, appId, false);
835 }
836
837
838
839
840
841 public CompletableFuture<Revision> createToken(Author author, String appId, boolean isAdmin) {
842 return createToken(author, appId, SECRET_PREFIX + UUID.randomUUID(), isAdmin);
843 }
844
845
846
847
848 public CompletableFuture<Revision> createToken(Author author, String appId, String secret) {
849 return createToken(author, appId, secret, false);
850 }
851
852
853
854
855 public CompletableFuture<Revision> createToken(Author author, String appId, String secret,
856 boolean isAdmin) {
857 requireNonNull(author, "author");
858 requireNonNull(appId, "appId");
859 requireNonNull(secret, "secret");
860
861 checkArgument(secret.startsWith(SECRET_PREFIX), "secret must start with: " + SECRET_PREFIX);
862
863 final Token newToken = new Token(appId, secret, isAdmin, UserAndTimestamp.of(author));
864 final JsonPointer appIdPath = JsonPointer.compile("/appIds" + encodeSegment(newToken.id()));
865 final String newTokenSecret = newToken.secret();
866 assert newTokenSecret != null;
867 final JsonPointer secretPath = JsonPointer.compile("/secrets" + encodeSegment(newTokenSecret));
868 final Change<JsonNode> change =
869 Change.ofJsonPatch(TOKEN_JSON,
870 asJsonArray(new TestAbsenceOperation(appIdPath),
871 new TestAbsenceOperation(secretPath),
872 new AddOperation(appIdPath, Jackson.valueToTree(newToken)),
873 new AddOperation(secretPath,
874 Jackson.valueToTree(newToken.id()))));
875 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author,
876 "Add a token: " + newToken.id(), change);
877 }
878
879
880
881
882 public CompletableFuture<Revision> destroyToken(Author author, String appId) {
883 requireNonNull(author, "author");
884 requireNonNull(appId, "appId");
885
886 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author,
887 "Delete the token: " + appId,
888 () -> tokenRepo
889 .fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
890 .thenApply(tokens -> {
891 final JsonPointer deletionPath =
892 JsonPointer.compile("/appIds" + encodeSegment(appId) +
893 "/deletion");
894 final Change<?> change = Change.ofJsonPatch(
895 TOKEN_JSON,
896 asJsonArray(new TestAbsenceOperation(deletionPath),
897 new AddOperation(deletionPath,
898 Jackson.valueToTree(
899 UserAndTimestamp.of(
900 author)))));
901 return HolderWithRevision.of(change, tokens.revision());
902 }));
903 }
904
905
906
907
908
909
910 public Revision purgeToken(Author author, String appId) {
911 requireNonNull(author, "author");
912 requireNonNull(appId, "appId");
913
914 final Collection<Project> projects = listProjectsWithoutDogma(projectManager.list()).values();
915
916 for (Project project : projects) {
917 final ProjectMetadata projectMetadata = fetchMetadata(project.name()).join().object();
918 final boolean containsTargetTokenInTheProject =
919 projectMetadata.tokens().values()
920 .stream()
921 .anyMatch(token -> token.appId().equals(appId));
922
923 if (containsTargetTokenInTheProject) {
924 removeToken(project.name(), author, appId, true).join();
925 }
926 }
927
928 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author, "Remove the token: " + appId,
929 () -> tokenRepo.fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
930 .thenApply(tokens -> {
931 final Token token = tokens.object().get(appId);
932 final JsonPointer appIdPath =
933 JsonPointer.compile("/appIds" + encodeSegment(appId));
934 final String secret = token.secret();
935 assert secret != null;
936 final JsonPointer secretPath =
937 JsonPointer.compile(
938 "/secrets" + encodeSegment(secret));
939 final Change<?> change = Change.ofJsonPatch(
940 TOKEN_JSON,
941 asJsonArray(new RemoveOperation(appIdPath),
942 new RemoveIfExistsOperation(secretPath)));
943 return HolderWithRevision.of(change, tokens.revision());
944 })).join();
945 }
946
947
948
949
950 public CompletableFuture<Revision> activateToken(Author author, String appId) {
951 requireNonNull(author, "author");
952 requireNonNull(appId, "appId");
953
954 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author,
955 "Enable the token: " + appId,
956 () -> tokenRepo
957 .fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
958 .thenApply(tokens -> {
959 final Token token = tokens.object().get(appId);
960 final JsonPointer removalPath =
961 JsonPointer.compile("/appIds" + encodeSegment(appId) +
962 "/deactivation");
963 final String secret = token.secret();
964 assert secret != null;
965 final JsonPointer secretPath =
966 JsonPointer.compile("/secrets" +
967 encodeSegment(secret));
968 final Change<JsonNode> change = Change.ofJsonPatch(
969 TOKEN_JSON,
970 asJsonArray(new RemoveOperation(removalPath),
971 new AddOperation(secretPath,
972 Jackson.valueToTree(appId))));
973 return HolderWithRevision.of(change, tokens.revision());
974 })
975 );
976 }
977
978
979
980
981 public CompletableFuture<Revision> deactivateToken(Author author, String appId) {
982 requireNonNull(author, "author");
983 requireNonNull(appId, "appId");
984
985 return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author,
986 "Disable the token: " + appId,
987 () -> tokenRepo
988 .fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
989 .thenApply(tokens -> {
990 final Token token = tokens.object().get(appId);
991 final JsonPointer removalPath =
992 JsonPointer.compile("/appIds" + encodeSegment(appId) +
993 "/deactivation");
994 final String secret = token.secret();
995 assert secret != null;
996 final JsonPointer secretPath =
997 JsonPointer.compile("/secrets" +
998 encodeSegment(secret));
999 final Change<?> change = Change.ofJsonPatch(
1000 TOKEN_JSON,
1001 asJsonArray(new TestAbsenceOperation(removalPath),
1002 new AddOperation(removalPath, Jackson.valueToTree(
1003 UserAndTimestamp.of(author))),
1004 new RemoveOperation(secretPath)));
1005 return HolderWithRevision.of(change, tokens.revision());
1006 }));
1007 }
1008
1009
1010
1011
1012 public CompletableFuture<Token> findTokenByAppId(String appId) {
1013 requireNonNull(appId, "appId");
1014 return tokenRepo.fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
1015 .thenApply(tokens -> tokens.object().get(appId));
1016 }
1017
1018
1019
1020
1021 public CompletableFuture<Token> findTokenBySecret(String secret) {
1022 requireNonNull(secret, "secret");
1023 validateSecret(secret);
1024 return tokenRepo.fetch(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, TOKEN_JSON)
1025 .thenApply(tokens -> tokens.object().findBySecret(secret));
1026 }
1027
1028
1029
1030
1031 private static void ensureProjectMember(ProjectMetadata project, User user) {
1032 requireNonNull(project, "project");
1033 requireNonNull(user, "user");
1034
1035 checkArgument(project.members().values().stream().anyMatch(member -> member.login().equals(user.id())),
1036 user.id() + " is not a member of the project '" + project.name() + '\'');
1037 }
1038
1039
1040
1041
1042 private static void ensureProjectToken(ProjectMetadata project, String appId) {
1043 requireNonNull(project, "project");
1044 requireNonNull(appId, "appId");
1045
1046 checkArgument(project.tokens().containsKey(appId),
1047 appId + " is not a token of the project '" + project.name() + '\'');
1048 }
1049
1050
1051
1052
1053
1054 private static JsonPointer perUserPermissionPointer(String repoName, String memberId) {
1055 return JsonPointer.compile("/repos" + encodeSegment(repoName) +
1056 "/perUserPermissions" + encodeSegment(memberId));
1057 }
1058
1059
1060
1061
1062
1063 private static JsonPointer perTokenPermissionPointer(String repoName, String appId) {
1064 return JsonPointer.compile("/repos" + encodeSegment(repoName) +
1065 "/perTokenPermissions" + encodeSegment(appId));
1066 }
1067 }