1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.server.internal.api;
18  
19  import static com.google.common.collect.ImmutableList.toImmutableList;
20  import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.checkStatusArgument;
21  import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.checkUnremoveArgument;
22  import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.returnOrThrow;
23  import static java.util.Objects.requireNonNull;
24  
25  import java.util.List;
26  import java.util.concurrent.CompletableFuture;
27  
28  import javax.annotation.Nullable;
29  
30  import com.fasterxml.jackson.databind.JsonNode;
31  
32  import com.linecorp.armeria.common.ContextAwareBlockingTaskExecutor;
33  import com.linecorp.armeria.server.ServiceRequestContext;
34  import com.linecorp.armeria.server.annotation.Consumes;
35  import com.linecorp.armeria.server.annotation.Delete;
36  import com.linecorp.armeria.server.annotation.Get;
37  import com.linecorp.armeria.server.annotation.Param;
38  import com.linecorp.armeria.server.annotation.Patch;
39  import com.linecorp.armeria.server.annotation.Post;
40  import com.linecorp.armeria.server.annotation.ProducesJson;
41  import com.linecorp.armeria.server.annotation.ResponseConverter;
42  import com.linecorp.armeria.server.annotation.StatusCode;
43  import com.linecorp.centraldogma.common.Author;
44  import com.linecorp.centraldogma.common.ProjectRole;
45  import com.linecorp.centraldogma.internal.api.v1.CreateProjectRequest;
46  import com.linecorp.centraldogma.internal.api.v1.ProjectDto;
47  import com.linecorp.centraldogma.server.command.CommandExecutor;
48  import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRole;
49  import com.linecorp.centraldogma.server.internal.api.auth.RequiresSystemAdministrator;
50  import com.linecorp.centraldogma.server.internal.api.converter.CreateApiResponseConverter;
51  import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager;
52  import com.linecorp.centraldogma.server.metadata.Member;
53  import com.linecorp.centraldogma.server.metadata.ProjectMetadata;
54  import com.linecorp.centraldogma.server.metadata.TokenRegistration;
55  import com.linecorp.centraldogma.server.metadata.User;
56  import com.linecorp.centraldogma.server.metadata.UserWithToken;
57  import com.linecorp.centraldogma.server.storage.project.Project;
58  
59  /**
60   * Annotated service object for managing projects.
61   */
62  @ProducesJson
63  public class ProjectServiceV1 extends AbstractService {
64  
65      private final ProjectApiManager projectApiManager;
66  
67      public ProjectServiceV1(ProjectApiManager projectApiManager, CommandExecutor executor) {
68          super(executor);
69          this.projectApiManager = requireNonNull(projectApiManager, "projectApiManager");
70      }
71  
72      /**
73       * GET /projects?status={status}
74       *
75       * <p>Returns the list of projects or removed projects.
76       */
77      @Get("/projects")
78      public CompletableFuture<List<ProjectDto>> listProjects(@Param @Nullable String status, User user) {
79          final ContextAwareBlockingTaskExecutor executor =
80                  ServiceRequestContext.current().blockingTaskExecutor();
81          if (status != null) {
82              checkStatusArgument(status);
83              return CompletableFuture.supplyAsync(() -> projectApiManager.listRemovedProjects().keySet()
84                                                                          .stream()
85                                                                          .map(ProjectDto::new)
86                                                                          .collect(toImmutableList()), executor);
87          }
88  
89          return CompletableFuture.supplyAsync(() -> {
90              return projectApiManager.listProjects(user).values().stream()
91                                      .map(project -> DtoConverter.convert(project, getUserRole(project, user)))
92                                      .collect(toImmutableList());
93          }, executor);
94      }
95  
96      private static ProjectRole getUserRole(Project project, User user) {
97          if (user.isSystemAdmin()) {
98              return ProjectRole.OWNER;
99          }
100 
101         final ProjectMetadata metadata = project.metadata();
102         if (metadata == null) {
103             // Metadata is null for the internal project which belongs to system administrators.
104             return ProjectRole.GUEST;
105         }
106 
107         ProjectRole role = null;
108         if (user instanceof UserWithToken) {
109             final String appId = ((UserWithToken) user).token().appId();
110             final TokenRegistration tokenRegistration = metadata.tokens().get(appId);
111             if (tokenRegistration != null) {
112                 role = tokenRegistration.role();
113             }
114         } else {
115             final Member member = metadata.memberOrDefault(user.id(), null);
116             if (member != null) {
117                 role = member.role();
118             }
119         }
120 
121         if (role == null) {
122             role = ProjectRole.GUEST;
123         }
124 
125         return role;
126     }
127 
128     /**
129      * POST /projects
130      *
131      * <p>Creates a new project.
132      */
133     @Post("/projects")
134     @StatusCode(201)
135     @ResponseConverter(CreateApiResponseConverter.class)
136     public CompletableFuture<ProjectDto> createProject(CreateProjectRequest request, Author author, User user) {
137         return projectApiManager.createProject(request.name(), author).handle(returnOrThrow(() -> {
138             final Project project = projectApiManager.getProject(request.name(), user);
139             return DtoConverter.convert(project, ProjectRole.OWNER);
140         }));
141     }
142 
143     /**
144      * GET /projects/{projectName}
145      *
146      * <p>Gets the {@link ProjectMetadata} of the specified {@code projectName}.
147      */
148     @Get("/projects/{projectName}")
149     @RequiresProjectRole(ProjectRole.MEMBER)
150     public CompletableFuture<ProjectMetadata> getProjectMetadata(@Param String projectName) {
151         return projectApiManager.getProjectMetadata(projectName);
152     }
153 
154     /**
155      * DELETE /projects/{projectName}
156      *
157      * <p>Removes a project.
158      */
159     @Delete("/projects/{projectName}")
160     @RequiresProjectRole(ProjectRole.OWNER)
161     public CompletableFuture<Void> removeProject(Project project, Author author) {
162         return projectApiManager.removeProject(project.name(), author);
163     }
164 
165     /**
166      * DELETE /projects/{projectName}/removed
167      *
168      * <p>Purges a project that was removed before.
169      */
170     @Delete("/projects/{projectName}/removed")
171     @RequiresProjectRole(ProjectRole.OWNER)
172     public CompletableFuture<Void> purgeProject(@Param String projectName, Author author) {
173         return projectApiManager.purgeProject(projectName, author);
174     }
175 
176     // TODO(minwoox): Migrate to /projects/{projectName}:unremove when it's supported.
177 
178     /**
179      * PATCH /projects/{projectName}
180      *
181      * <p>Patches a project with the JSON_PATCH. Currently, only unremove project operation is supported.
182      */
183     @Consumes("application/json-patch+json")
184     @Patch("/projects/{projectName}")
185     @RequiresSystemAdministrator
186     public CompletableFuture<ProjectDto> patchProject(@Param String projectName, JsonNode node, Author author,
187                                                       User user) {
188         checkUnremoveArgument(node);
189         return projectApiManager.unremoveProject(projectName, author)
190                                 .handle(returnOrThrow(() -> DtoConverter.convert(
191                                         projectApiManager.getProject(projectName, user), ProjectRole.OWNER)));
192     }
193 }