1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.internal.api;
18
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static java.util.Objects.requireNonNull;
21
22 import java.io.UnsupportedEncodingException;
23 import java.net.URLDecoder;
24 import java.nio.charset.StandardCharsets;
25 import java.util.Collection;
26 import java.util.List;
27 import java.util.concurrent.CompletableFuture;
28 import java.util.function.Function;
29
30 import com.fasterxml.jackson.annotation.JsonCreator;
31 import com.fasterxml.jackson.annotation.JsonProperty;
32 import com.fasterxml.jackson.core.type.TypeReference;
33 import com.google.common.base.MoreObjects;
34
35 import com.linecorp.armeria.common.util.Exceptions;
36 import com.linecorp.armeria.server.annotation.Consumes;
37 import com.linecorp.armeria.server.annotation.Delete;
38 import com.linecorp.armeria.server.annotation.ExceptionHandler;
39 import com.linecorp.armeria.server.annotation.Param;
40 import com.linecorp.armeria.server.annotation.Patch;
41 import com.linecorp.armeria.server.annotation.Post;
42 import com.linecorp.armeria.server.annotation.ProducesJson;
43 import com.linecorp.centraldogma.common.Author;
44 import com.linecorp.centraldogma.common.Revision;
45 import com.linecorp.centraldogma.internal.Jackson;
46 import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch;
47 import com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation;
48 import com.linecorp.centraldogma.internal.jsonpatch.ReplaceOperation;
49 import com.linecorp.centraldogma.server.QuotaConfig;
50 import com.linecorp.centraldogma.server.internal.api.auth.RequiresAdministrator;
51 import com.linecorp.centraldogma.server.internal.api.auth.RequiresRole;
52 import com.linecorp.centraldogma.server.metadata.MetadataService;
53 import com.linecorp.centraldogma.server.metadata.PerRolePermissions;
54 import com.linecorp.centraldogma.server.metadata.Permission;
55 import com.linecorp.centraldogma.server.metadata.ProjectRole;
56 import com.linecorp.centraldogma.server.metadata.Token;
57 import com.linecorp.centraldogma.server.metadata.User;
58
59
60
61
62 @ProducesJson
63 @RequiresRole(roles = ProjectRole.OWNER)
64 @ExceptionHandler(HttpApiExceptionHandler.class)
65 public class MetadataApiService {
66
67 private static final TypeReference<Collection<Permission>> permissionsTypeRef =
68 new TypeReference<Collection<Permission>>() {};
69
70 private final MetadataService mds;
71 private final Function<String, String> loginNameNormalizer;
72
73 public MetadataApiService(MetadataService mds, Function<String, String> loginNameNormalizer) {
74 this.mds = requireNonNull(mds, "mds");
75 this.loginNameNormalizer = requireNonNull(loginNameNormalizer, "loginNameNormalizer");
76 }
77
78
79
80
81
82
83 @Post("/metadata/{projectName}/members")
84 public CompletableFuture<Revision> addMember(@Param String projectName,
85 IdentifierWithRole request,
86 Author author) {
87 final ProjectRole role = toProjectRole(request.role());
88 final User member = new User(loginNameNormalizer.apply(request.id()));
89 return mds.addMember(author, projectName, member, role);
90 }
91
92
93
94
95
96
97
98 @Patch("/metadata/{projectName}/members/{memberId}")
99 @Consumes("application/json-patch+json")
100 public CompletableFuture<Revision> updateMember(@Param String projectName,
101 @Param String memberId,
102 JsonPatch jsonPatch,
103 Author author) {
104 final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/role");
105 final ProjectRole role = ProjectRole.of(operation.value());
106 final User member = new User(loginNameNormalizer.apply(urlDecode(memberId)));
107 return mds.getMember(projectName, member)
108 .thenCompose(unused -> mds.updateMemberRole(author, projectName, member, role));
109 }
110
111
112
113
114
115
116 @Delete("/metadata/{projectName}/members/{memberId}")
117 public CompletableFuture<Revision> removeMember(@Param String projectName,
118 @Param String memberId,
119 Author author) {
120 final User member = new User(loginNameNormalizer.apply(urlDecode(memberId)));
121 return mds.getMember(projectName, member)
122 .thenCompose(unused -> mds.removeMember(author, projectName, member));
123 }
124
125
126
127
128
129
130 @Post("/metadata/{projectName}/tokens")
131 public CompletableFuture<Revision> addToken(@Param String projectName,
132 IdentifierWithRole request,
133 Author author) {
134 final ProjectRole role = toProjectRole(request.role());
135 return mds.findTokenByAppId(request.id())
136 .thenCompose(token -> mds.addToken(author, projectName, token.appId(), role));
137 }
138
139
140
141
142
143
144
145 @Patch("/metadata/{projectName}/tokens/{appId}")
146 @Consumes("application/json-patch+json")
147 public CompletableFuture<Revision> updateTokenRole(@Param String projectName,
148 @Param String appId,
149 JsonPatch jsonPatch,
150 Author author) {
151 final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/role");
152 final ProjectRole role = ProjectRole.of(operation.value());
153 return mds.findTokenByAppId(appId)
154 .thenCompose(token -> mds.updateTokenRole(author, projectName, token, role));
155 }
156
157
158
159
160
161
162 @Delete("/metadata/{projectName}/tokens/{appId}")
163 public CompletableFuture<Revision> removeToken(@Param String projectName,
164 @Param String appId,
165 Author author) {
166 return mds.findTokenByAppId(appId)
167 .thenCompose(token -> mds.removeToken(author, projectName, token.appId()));
168 }
169
170
171
172
173
174
175
176 @Post("/metadata/{projectName}/repos/{repoName}/perm/role")
177 public CompletableFuture<Revision> updateRolePermission(@Param String projectName,
178 @Param String repoName,
179 PerRolePermissions newPermission,
180 Author author) {
181 return mds.updatePerRolePermissions(author, projectName, repoName, newPermission);
182 }
183
184
185
186
187
188
189
190 @Post("/metadata/{projectName}/repos/{repoName}/perm/users")
191 public CompletableFuture<Revision> addSpecificUserPermission(
192 @Param String projectName,
193 @Param String repoName,
194 IdentifierWithPermissions memberWithPermissions,
195 Author author) {
196 final User member = new User(loginNameNormalizer.apply(memberWithPermissions.id()));
197 return mds.addPerUserPermission(author, projectName, repoName,
198 member, memberWithPermissions.permissions());
199 }
200
201
202
203
204
205
206
207 @Patch("/metadata/{projectName}/repos/{repoName}/perm/users/{memberId}")
208 @Consumes("application/json-patch+json")
209 public CompletableFuture<Revision> updateSpecificUserPermission(@Param String projectName,
210 @Param String repoName,
211 @Param String memberId,
212 JsonPatch jsonPatch,
213 Author author) {
214 final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/permissions");
215 final Collection<Permission> permissions = Jackson.convertValue(operation.value(), permissionsTypeRef);
216 final User member = new User(loginNameNormalizer.apply(urlDecode(memberId)));
217 return mds.findPermissions(projectName, repoName, member)
218 .thenCompose(unused -> mds.updatePerUserPermission(author,
219 projectName, repoName, member,
220 permissions));
221 }
222
223
224
225
226
227
228
229 @Delete("/metadata/{projectName}/repos/{repoName}/perm/users/{memberId}")
230 public CompletableFuture<Revision> removeSpecificUserPermission(@Param String projectName,
231 @Param String repoName,
232 @Param String memberId,
233 Author author) {
234 final User member = new User(loginNameNormalizer.apply(urlDecode(memberId)));
235 return mds.findPermissions(projectName, repoName, member)
236 .thenCompose(unused -> mds.removePerUserPermission(author, projectName,
237 repoName, member));
238 }
239
240
241
242
243
244
245
246 @Post("/metadata/{projectName}/repos/{repoName}/perm/tokens")
247 public CompletableFuture<Revision> addSpecificTokenPermission(
248 @Param String projectName,
249 @Param String repoName,
250 IdentifierWithPermissions tokenWithPermissions,
251 Author author) {
252 return mds.addPerTokenPermission(author, projectName, repoName,
253 tokenWithPermissions.id(), tokenWithPermissions.permissions());
254 }
255
256
257
258
259
260
261
262 @Patch("/metadata/{projectName}/repos/{repoName}/perm/tokens/{appId}")
263 @Consumes("application/json-patch+json")
264 public CompletableFuture<Revision> updateSpecificTokenPermission(@Param String projectName,
265 @Param String repoName,
266 @Param String appId,
267 JsonPatch jsonPatch,
268 Author author) {
269 final ReplaceOperation operation = ensureSingleReplaceOperation(jsonPatch, "/permissions");
270 final Collection<Permission> permissions = Jackson.convertValue(operation.value(), permissionsTypeRef);
271 return mds.findTokenByAppId(appId)
272 .thenCompose(token -> mds.updatePerTokenPermission(
273 author, projectName, repoName, appId, permissions));
274 }
275
276
277
278
279
280
281
282 @Delete("/metadata/{projectName}/repos/{repoName}/perm/tokens/{appId}")
283 public CompletableFuture<Revision> removeSpecificTokenPermission(@Param String projectName,
284 @Param String repoName,
285 @Param String appId,
286 Author author) {
287 return mds.findTokenByAppId(appId)
288 .thenCompose(token -> mds.removePerTokenPermission(author,
289 projectName, repoName, appId));
290 }
291
292
293
294
295
296
297
298 @Patch("/metadata/{projectName}/repos/{repoName}/quota/write")
299 @Consumes("application/json-patch+json")
300 @RequiresAdministrator
301 public CompletableFuture<Revision> updateWriteQuota(@Param String projectName,
302 @Param String repoName,
303 QuotaConfig quota,
304 Author author) {
305 return mds.updateWriteQuota(author, projectName, repoName, quota);
306 }
307
308 private static ReplaceOperation ensureSingleReplaceOperation(JsonPatch patch, String expectedPath) {
309 final List<JsonPatchOperation> operations = patch.operations();
310 checkArgument(operations.size() == 1,
311 "Should be a single JSON patch operation in the list: " + operations.size());
312
313 final JsonPatchOperation operation = patch.operations().get(0);
314 checkArgument(operation instanceof ReplaceOperation,
315 "Should be a replace operation: " + operation);
316
317 checkArgument(expectedPath.equals(operation.path().toString()),
318 "Invalid path value: " + operation.path());
319
320 return (ReplaceOperation) operation;
321 }
322
323 private static String urlDecode(String input) {
324 try {
325
326 return URLDecoder.decode(input, StandardCharsets.UTF_8.name());
327 } catch (UnsupportedEncodingException e) {
328 return Exceptions.throwUnsafely(e);
329 }
330 }
331
332 private static ProjectRole toProjectRole(String roleStr) {
333 final ProjectRole role = ProjectRole.valueOf(requireNonNull(roleStr, "roleStr"));
334 checkArgument(role == ProjectRole.OWNER || role == ProjectRole.MEMBER,
335 "Invalid role: " + role +
336 " (expected: '" + ProjectRole.OWNER + "' or '" + ProjectRole.MEMBER + "')");
337 return role;
338 }
339
340
341
342 static final class IdentifierWithRole {
343
344 private final String id;
345 private final String role;
346
347 @JsonCreator
348 IdentifierWithRole(@JsonProperty("id") String id,
349 @JsonProperty("role") String role) {
350 this.id = requireNonNull(id, "id");
351 this.role = requireNonNull(role, "role");
352 }
353
354 @JsonProperty
355 public String id() {
356 return id;
357 }
358
359 @JsonProperty
360 public String role() {
361 return role;
362 }
363
364 @Override
365 public String toString() {
366 return MoreObjects.toStringHelper(this)
367 .add("id", id())
368 .add("role", role())
369 .toString();
370 }
371 }
372
373 static final class IdentifierWithPermissions {
374
375 private final String id;
376 private final Collection<Permission> permissions;
377
378 @JsonCreator
379 IdentifierWithPermissions(@JsonProperty("id") String id,
380 @JsonProperty("permissions") Collection<Permission> permissions) {
381 this.id = requireNonNull(id, "id");
382 this.permissions = requireNonNull(permissions, "permissions");
383 }
384
385 @JsonProperty
386 public String id() {
387 return id;
388 }
389
390 @JsonProperty
391 public Collection<Permission> permissions() {
392 return permissions;
393 }
394
395 @Override
396 public String toString() {
397 return MoreObjects.toStringHelper(this)
398 .add("id", id())
399 .add("permissions", permissions())
400 .toString();
401 }
402 }
403 }