1   /*
2    * Copyright 2018 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.auth;
18  
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static com.google.common.base.Strings.isNullOrEmpty;
21  import static com.google.common.collect.ImmutableList.toImmutableList;
22  import static java.util.Objects.requireNonNull;
23  
24  import java.util.Set;
25  import java.util.function.Function;
26  
27  import com.google.common.collect.ImmutableSet;
28  
29  import com.linecorp.armeria.common.HttpRequest;
30  import com.linecorp.armeria.common.HttpResponse;
31  import com.linecorp.armeria.common.HttpStatus;
32  import com.linecorp.armeria.common.util.Exceptions;
33  import com.linecorp.armeria.server.HttpService;
34  import com.linecorp.armeria.server.ServiceRequestContext;
35  import com.linecorp.armeria.server.SimpleDecoratingHttpService;
36  import com.linecorp.armeria.server.annotation.DecoratorFactoryFunction;
37  import com.linecorp.centraldogma.common.ProjectNotFoundException;
38  import com.linecorp.centraldogma.common.RepositoryNotFoundException;
39  import com.linecorp.centraldogma.server.internal.admin.auth.AuthUtil;
40  import com.linecorp.centraldogma.server.internal.api.HttpApiUtil;
41  import com.linecorp.centraldogma.server.metadata.MetadataService;
42  import com.linecorp.centraldogma.server.metadata.MetadataServiceInjector;
43  import com.linecorp.centraldogma.server.metadata.ProjectRole;
44  import com.linecorp.centraldogma.server.metadata.User;
45  
46  /**
47   * A decorator for checking the project role of the user sent a request.
48   */
49  public final class RequiresRoleDecorator extends SimpleDecoratingHttpService {
50  
51      private final Set<ProjectRole> accessibleRoles;
52      private final String roleNames;
53  
54      RequiresRoleDecorator(HttpService delegate, Set<ProjectRole> accessibleRoles) {
55          super(delegate);
56          this.accessibleRoles = ImmutableSet.copyOf(requireNonNull(accessibleRoles, "accessibleRoles"));
57          roleNames = String.join(",",
58                                  accessibleRoles.stream().map(ProjectRole::name).collect(toImmutableList()));
59      }
60  
61      @Override
62      public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
63          final MetadataService mds = MetadataServiceInjector.getMetadataService(ctx);
64          final User user = AuthUtil.currentUser(ctx);
65  
66          final String projectName = ctx.pathParam("projectName");
67          checkArgument(!isNullOrEmpty(projectName), "no project name is specified");
68  
69          try {
70              return HttpResponse.from(mds.findRole(projectName, user).handle((role, cause) -> {
71                  if (cause != null) {
72                      return handleException(ctx, cause);
73                  }
74                  if (!user.isAdmin() && !accessibleRoles.contains(role)) {
75                      return HttpApiUtil.throwResponse(
76                              ctx, HttpStatus.FORBIDDEN,
77                              "You must have one of the following roles to access the project '%s': %s",
78                              projectName, roleNames);
79                  }
80                  try {
81                      return unwrap().serve(ctx, req);
82                  } catch (Exception e) {
83                      return Exceptions.throwUnsafely(e);
84                  }
85              }));
86          } catch (Throwable cause) {
87              return handleException(ctx, cause);
88          }
89      }
90  
91      static HttpResponse handleException(ServiceRequestContext ctx, Throwable cause) {
92          cause = Exceptions.peel(cause);
93          if (cause instanceof RepositoryNotFoundException ||
94              cause instanceof ProjectNotFoundException) {
95              return HttpApiUtil.newResponse(ctx, HttpStatus.NOT_FOUND, cause);
96          } else {
97              return Exceptions.throwUnsafely(cause);
98          }
99      }
100 
101     public static final class RequiresRoleDecoratorFactory
102             implements DecoratorFactoryFunction<RequiresRole> {
103 
104         @Override
105         public Function<? super HttpService, ? extends HttpService> newDecorator(RequiresRole parameter) {
106             return delegate -> new RequiresRoleDecorator(delegate, ImmutableSet.copyOf(parameter.roles()));
107         }
108     }
109 }