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.util.Collection;
23 import java.util.concurrent.CompletableFuture;
24
25 import javax.annotation.Nullable;
26
27 import com.fasterxml.jackson.databind.JsonNode;
28 import com.google.common.collect.ImmutableList;
29 import com.google.common.collect.ImmutableMap;
30
31 import com.linecorp.armeria.common.HttpHeaderNames;
32 import com.linecorp.armeria.common.HttpStatus;
33 import com.linecorp.armeria.common.ResponseHeaders;
34 import com.linecorp.armeria.server.ServiceRequestContext;
35 import com.linecorp.armeria.server.annotation.Consumes;
36 import com.linecorp.armeria.server.annotation.Delete;
37 import com.linecorp.armeria.server.annotation.ExceptionHandler;
38 import com.linecorp.armeria.server.annotation.Get;
39 import com.linecorp.armeria.server.annotation.HttpResult;
40 import com.linecorp.armeria.server.annotation.Param;
41 import com.linecorp.armeria.server.annotation.Patch;
42 import com.linecorp.armeria.server.annotation.Post;
43 import com.linecorp.armeria.server.annotation.ProducesJson;
44 import com.linecorp.armeria.server.annotation.ResponseConverter;
45 import com.linecorp.armeria.server.annotation.StatusCode;
46 import com.linecorp.centraldogma.common.Author;
47 import com.linecorp.centraldogma.common.Revision;
48 import com.linecorp.centraldogma.internal.Jackson;
49 import com.linecorp.centraldogma.server.command.CommandExecutor;
50 import com.linecorp.centraldogma.server.internal.api.auth.RequiresAdministrator;
51 import com.linecorp.centraldogma.server.internal.api.converter.CreateApiResponseConverter;
52 import com.linecorp.centraldogma.server.metadata.MetadataService;
53 import com.linecorp.centraldogma.server.metadata.Token;
54 import com.linecorp.centraldogma.server.metadata.Tokens;
55 import com.linecorp.centraldogma.server.metadata.User;
56
57
58
59
60 @ProducesJson
61 @ExceptionHandler(HttpApiExceptionHandler.class)
62 public class TokenService extends AbstractService {
63
64 private static final JsonNode activation = Jackson.valueToTree(
65 ImmutableList.of(
66 ImmutableMap.of("op", "replace",
67 "path", "/status",
68 "value", "active")));
69 private static final JsonNode deactivation = Jackson.valueToTree(
70 ImmutableList.of(
71 ImmutableMap.of("op", "replace",
72 "path", "/status",
73 "value", "inactive")));
74
75 private final MetadataService mds;
76
77 public TokenService(CommandExecutor executor, MetadataService mds) {
78 super(executor);
79 this.mds = requireNonNull(mds, "mds");
80 }
81
82
83
84
85
86
87 @Get("/tokens")
88 public CompletableFuture<Collection<Token>> listTokens(User loginUser) {
89 if (loginUser.isAdmin()) {
90 return mds.getTokens()
91 .thenApply(tokens -> tokens.appIds().values());
92 } else {
93 return mds.getTokens()
94 .thenApply(Tokens::withoutSecret)
95 .thenApply(tokens -> tokens.appIds().values());
96 }
97 }
98
99
100
101
102
103
104 @Post("/tokens")
105 @StatusCode(201)
106 @ResponseConverter(CreateApiResponseConverter.class)
107 public CompletableFuture<HttpResult<Token>> createToken(@Param String appId,
108 @Param boolean isAdmin,
109 @Param @Nullable String secret,
110 Author author, User loginUser) {
111 checkArgument(!isAdmin || loginUser.isAdmin(),
112 "Only administrators are allowed to create an admin-level token.");
113
114 checkArgument(secret == null || loginUser.isAdmin(),
115 "Only administrators are allowed to create a new token from the given secret string");
116
117 final CompletableFuture<Revision> tokenFuture;
118 if (secret != null) {
119 tokenFuture = mds.createToken(author, appId, secret, isAdmin);
120 } else {
121 tokenFuture = mds.createToken(author, appId, isAdmin);
122 }
123 return tokenFuture
124 .thenCompose(unused -> mds.findTokenByAppId(appId))
125 .thenApply(token -> {
126 final ResponseHeaders headers = ResponseHeaders.of(HttpStatus.CREATED,
127 HttpHeaderNames.LOCATION,
128 "/tokens/" + appId);
129 return HttpResult.of(headers, token);
130 });
131 }
132
133
134
135
136
137
138 @Delete("/tokens/{appId}")
139 public CompletableFuture<Token> deleteToken(ServiceRequestContext ctx,
140 @Param String appId,
141 Author author, User loginUser) {
142 return getTokenOrRespondForbidden(ctx, appId, loginUser).thenCompose(
143 token -> mds.destroyToken(author, appId)
144 .thenApply(unused -> token.withoutSecret()));
145 }
146
147
148
149
150
151
152 @Delete("/tokens/{appId}/removed")
153 @RequiresAdministrator
154 public CompletableFuture<Token> purgeToken(ServiceRequestContext ctx,
155 @Param String appId,
156 Author author, User loginUser) {
157 return getTokenOrRespondForbidden(ctx, appId, loginUser).thenApplyAsync(
158 token -> {
159 mds.purgeToken(author, appId);
160 return token.withoutSecret();
161 }, ctx.blockingTaskExecutor());
162 }
163
164
165
166
167
168
169 @Patch("/tokens/{appId}")
170 @Consumes("application/json-patch+json")
171 public CompletableFuture<Token> updateToken(ServiceRequestContext ctx,
172 @Param String appId,
173 JsonNode node, Author author, User loginUser) {
174 return getTokenOrRespondForbidden(ctx, appId, loginUser).thenCompose(
175 token -> {
176 if (token.isDeleted()) {
177 throw new IllegalArgumentException(
178 "You can't update the status of the token scheduled for deletion.");
179 }
180 if (node.equals(activation)) {
181 return mds.activateToken(author, appId)
182 .thenApply(unused -> token.withoutSecret());
183 }
184 if (node.equals(deactivation)) {
185 return mds.deactivateToken(author, appId)
186 .thenApply(unused -> token.withoutSecret());
187 }
188 throw new IllegalArgumentException("Unsupported JSON patch: " + node +
189 " (expected: " + activation + " or " + deactivation +
190 ')');
191 }
192 );
193 }
194
195 private CompletableFuture<Token> getTokenOrRespondForbidden(ServiceRequestContext ctx,
196 String appId, User loginUser) {
197 return mds.findTokenByAppId(appId).thenApply(token -> {
198
199 if (!loginUser.isAdmin() &&
200 !token.creation().user().equals(loginUser.id())) {
201 return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
202 "Unauthorized token: %s", token);
203 }
204 return token;
205 });
206 }
207 }