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.linecorp.centraldogma.server.internal.api.auth.RequiresRoleDecorator.handleException;
22  import static java.util.Objects.requireNonNull;
23  
24  import java.util.Collection;
25  import java.util.concurrent.CompletionStage;
26  import java.util.function.Function;
27  
28  import com.linecorp.armeria.common.HttpRequest;
29  import com.linecorp.armeria.common.HttpResponse;
30  import com.linecorp.armeria.common.HttpStatus;
31  import com.linecorp.armeria.common.util.Exceptions;
32  import com.linecorp.armeria.server.HttpService;
33  import com.linecorp.armeria.server.ServiceRequestContext;
34  import com.linecorp.armeria.server.SimpleDecoratingHttpService;
35  import com.linecorp.armeria.server.annotation.Decorator;
36  import com.linecorp.armeria.server.annotation.DecoratorFactoryFunction;
37  import com.linecorp.centraldogma.server.internal.admin.auth.AuthUtil;
38  import com.linecorp.centraldogma.server.internal.api.HttpApiUtil;
39  import com.linecorp.centraldogma.server.metadata.MetadataService;
40  import com.linecorp.centraldogma.server.metadata.MetadataServiceInjector;
41  import com.linecorp.centraldogma.server.metadata.Permission;
42  import com.linecorp.centraldogma.server.metadata.User;
43  import com.linecorp.centraldogma.server.storage.project.Project;
44  
45  /**
46   * A {@link Decorator} to allow a request from a user who has the specified {@link Permission}.
47   */
48  public final class RequiresPermissionDecorator extends SimpleDecoratingHttpService {
49  
50      private final Permission requiredPermission;
51  
52      RequiresPermissionDecorator(HttpService delegate, Permission requiredPermission) {
53          super(delegate);
54          this.requiredPermission = requireNonNull(requiredPermission, "requiredPermission");
55      }
56  
57      @Override
58      public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
59          final MetadataService mds = MetadataServiceInjector.getMetadataService(ctx);
60          final User user = AuthUtil.currentUser(ctx);
61  
62          final String projectName = ctx.pathParam("projectName");
63          checkArgument(!isNullOrEmpty(projectName), "no project name is specified");
64          final String repoName = ctx.pathParam("repoName");
65          checkArgument(!isNullOrEmpty(repoName), "no repository name is specified");
66  
67          if (Project.REPO_DOGMA.equals(repoName)) {
68              if (!user.isAdmin()) {
69                  return throwForbiddenResponse(ctx, projectName, repoName, "administrator");
70              }
71              return unwrap().serve(ctx, req);
72          }
73          return serveUserRepo(ctx, req, mds, user, projectName, repoName);
74      }
75  
76      private static HttpResponse throwForbiddenResponse(ServiceRequestContext ctx, String projectName,
77                                                         String repoName, String adminOrOwner) {
78          return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
79                                           "Repository '%s/%s' can be accessed only by an %s.",
80                                           projectName, repoName, adminOrOwner);
81      }
82  
83      private HttpResponse serveUserRepo(ServiceRequestContext ctx, HttpRequest req,
84                                         MetadataService mds, User user,
85                                         String projectName, String repoName) throws Exception {
86          final CompletionStage<Collection<Permission>> f;
87          try {
88              f = mds.findPermissions(projectName, repoName, user);
89          } catch (Throwable cause) {
90              return handleException(ctx, cause);
91          }
92  
93          return HttpResponse.of(f.handle((permission, cause) -> {
94              if (cause != null) {
95                  return handleException(ctx, cause);
96              }
97              if (!permission.contains(requiredPermission)) {
98                  return HttpApiUtil.throwResponse(
99                          ctx, HttpStatus.FORBIDDEN,
100                         "You must have %s permission for repository '%s/%s'.",
101                         requiredPermission, projectName, repoName);
102             }
103             try {
104                 return unwrap().serve(ctx, req);
105             } catch (Exception e) {
106                 return Exceptions.throwUnsafely(e);
107             }
108         }));
109     }
110 
111     /**
112      * A {@link DecoratorFactoryFunction} which creates a {@link RequiresPermissionDecorator} with a read
113      * {@link Permission}.
114      */
115     public static final class RequiresReadPermissionDecoratorFactory
116             implements DecoratorFactoryFunction<RequiresReadPermission> {
117         @Override
118         public Function<? super HttpService, ? extends HttpService>
119         newDecorator(RequiresReadPermission parameter) {
120             return delegate -> new RequiresPermissionDecorator(delegate, Permission.READ);
121         }
122     }
123 
124     /**
125      * A {@link DecoratorFactoryFunction} which creates a {@link RequiresPermissionDecorator} with a write
126      * {@link Permission}.
127      */
128     public static final class RequiresWritePermissionDecoratorFactory
129             implements DecoratorFactoryFunction<RequiresWritePermission> {
130         @Override
131         public Function<? super HttpService, ? extends HttpService>
132         newDecorator(RequiresWritePermission parameter) {
133             return delegate -> new RequiresPermissionDecorator(delegate, Permission.WRITE);
134         }
135     }
136 }