1   /*
2    * Copyright 2024 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  package com.linecorp.centraldogma.server.internal.storage.project;
17  
18  import static com.linecorp.centraldogma.internal.Util.INTERNAL_PROJECT_PREFIX;
19  import static com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer.INTERNAL_PROJECT_DOGMA;
20  
21  import java.time.Instant;
22  import java.util.Collections;
23  import java.util.LinkedHashMap;
24  import java.util.Map;
25  import java.util.concurrent.CompletableFuture;
26  
27  import javax.annotation.Nullable;
28  
29  import com.linecorp.centraldogma.common.Author;
30  import com.linecorp.centraldogma.common.PermissionException;
31  import com.linecorp.centraldogma.common.Revision;
32  import com.linecorp.centraldogma.server.command.Command;
33  import com.linecorp.centraldogma.server.command.CommandExecutor;
34  import com.linecorp.centraldogma.server.internal.admin.auth.AuthUtil;
35  import com.linecorp.centraldogma.server.metadata.MetadataService;
36  import com.linecorp.centraldogma.server.metadata.ProjectMetadata;
37  import com.linecorp.centraldogma.server.metadata.User;
38  import com.linecorp.centraldogma.server.storage.project.Project;
39  import com.linecorp.centraldogma.server.storage.project.ProjectManager;
40  
41  /**
42   * A wrapper class of {@link ProjectManager} which prevents accessing internal projects
43   * from unprivileged requests.
44   */
45  public final class ProjectApiManager {
46  
47      private final ProjectManager projectManager;
48      private final CommandExecutor commandExecutor;
49      private final MetadataService metadataService;
50  
51      public ProjectApiManager(ProjectManager projectManager, CommandExecutor commandExecutor,
52                               MetadataService metadataService) {
53          this.projectManager = projectManager;
54          this.commandExecutor = commandExecutor;
55          this.metadataService = metadataService;
56      }
57  
58      public Map<String, Project> listProjects(@Nullable User user) {
59          final Map<String, Project> projects = projectManager.list();
60          if (isSystemAdmin()) {
61              return projects;
62          }
63  
64          return listProjectsWithoutInternal(projects, user);
65      }
66  
67      private static boolean isSystemAdmin() {
68          final User currentUserOrNull = AuthUtil.currentUserOrNull();
69          if (currentUserOrNull == null) {
70              return false;
71          }
72  
73          return currentUserOrNull.isSystemAdmin();
74      }
75  
76      public static Map<String, Project> listProjectsWithoutInternal(Map<String, Project> projects,
77                                                                     @Nullable User user) {
78          final Map<String, Project> result = new LinkedHashMap<>(projects.size() - 1);
79          for (Map.Entry<String, Project> entry : projects.entrySet()) {
80              if (isInternalProject(entry.getKey())) {
81                  if (user != null) {
82                      final ProjectMetadata metadata = entry.getValue().metadata();
83                      if (metadata != null) {
84                          // Only show internal projects to the members of the project.
85                          if (metadata.memberOrDefault(user.id(), null) != null) {
86                              result.put(entry.getKey(), entry.getValue());
87                          }
88                      }
89                  }
90              } else {
91                  result.put(entry.getKey(), entry.getValue());
92              }
93          }
94          return Collections.unmodifiableMap(result);
95      }
96  
97      public Map<String, Instant> listRemovedProjects() {
98          return projectManager.listRemoved();
99      }
100 
101     public CompletableFuture<Void> createProject(String projectName, Author author) {
102         checkInternalProject(projectName, "create");
103         return commandExecutor.execute(Command.createProject(author, projectName));
104     }
105 
106     private static void checkInternalProject(String projectName, String operation) {
107         if (isInternalProject(projectName)) {
108             throw new IllegalArgumentException("Cannot " + operation + ' ' + projectName);
109         }
110     }
111 
112     public CompletableFuture<ProjectMetadata> getProjectMetadata(String projectName) {
113         return metadataService.getProject(projectName);
114     }
115 
116     public CompletableFuture<Void> removeProject(String projectName, Author author) {
117         checkInternalProject(projectName, "remove");
118         // Metadata must be updated first because it cannot be updated if the project is removed.
119         return metadataService.removeProject(author, projectName)
120                               .thenCompose(unused -> commandExecutor.execute(
121                                       Command.removeProject(author, projectName)));
122     }
123 
124     public CompletableFuture<Void> purgeProject(String projectName, Author author) {
125         checkInternalProject(projectName, "purge");
126         return commandExecutor.execute(Command.purgeProject(author, projectName));
127     }
128 
129     public CompletableFuture<Revision> unremoveProject(String projectName, Author author) {
130         checkInternalProject(projectName, "unremove");
131         // Restore the project first then update its metadata as 'active'.
132         return commandExecutor.execute(Command.unremoveProject(author, projectName))
133                               .thenCompose(unused -> metadataService.restoreProject(author, projectName));
134     }
135 
136     public Project getProject(String projectName) {
137         return getProject(projectName, AuthUtil.currentUserOrNull());
138     }
139 
140     public Project getProject(String projectName, @Nullable User user) {
141         final Project project = projectManager.get(projectName);
142 
143         if (!isInternalProject(projectName)) {
144             return project;
145         }
146 
147         if (user == null) {
148             throw new IllegalArgumentException("Cannot access " + projectName);
149         }
150 
151         if (user.isSystemAdmin()) {
152             return project;
153         }
154         final ProjectMetadata metadata = project.metadata();
155         if (metadata != null) {
156             // Only show internal projects to the members of the project.
157             if (metadata.memberOrDefault(user.id(), null) != null) {
158                 return project;
159             }
160         }
161         throw new PermissionException("Cannot access " + projectName);
162     }
163 
164     private static boolean isInternalProject(String projectName) {
165         return projectName.startsWith(INTERNAL_PROJECT_PREFIX) || INTERNAL_PROJECT_DOGMA.equals(projectName);
166     }
167 
168     public boolean exists(String projectName) {
169         if (isInternalProject(projectName) && !isSystemAdmin()) {
170             throw new IllegalArgumentException("Cannot access " + projectName);
171         }
172         return projectManager.exists(projectName);
173     }
174 }