View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.restapi.v2;
20  
21  import java.util.List;
22  import java.util.Objects;
23  import java.util.Optional;
24  import java.util.stream.Collectors;
25  import java.util.stream.Stream;
26  
27  import org.apache.logging.log4j.LogManager;
28  import org.mycore.access.MCRAccessManager;
29  import org.mycore.access.mcrimpl.MCRAccessControlSystem;
30  import org.mycore.frontend.jersey.access.MCRRequestScopeACL;
31  import org.mycore.restapi.converter.MCRDetailLevel;
32  import org.mycore.restapi.v2.access.MCRRestAPIACLPermission;
33  import org.mycore.restapi.v2.access.MCRRestAccessManager;
34  import org.mycore.restapi.v2.annotation.MCRRestRequiredPermission;
35  
36  import jakarta.annotation.Priority;
37  import jakarta.ws.rs.ForbiddenException;
38  import jakarta.ws.rs.HttpMethod;
39  import jakarta.ws.rs.Path;
40  import jakarta.ws.rs.Priorities;
41  import jakarta.ws.rs.container.ContainerRequestContext;
42  import jakarta.ws.rs.container.ContainerRequestFilter;
43  import jakarta.ws.rs.container.ResourceInfo;
44  import jakarta.ws.rs.core.Context;
45  import jakarta.ws.rs.core.MultivaluedMap;
46  import jakarta.ws.rs.core.Response;
47  
48  @Priority(Priorities.AUTHORIZATION)
49  public class MCRRestAuthorizationFilter implements ContainerRequestFilter {
50  
51      public static final String PARAM_CLASSID = "classid";
52  
53      public static final String PARAM_MCRID = "mcrid";
54  
55      public static final String PARAM_DERID = "derid";
56  
57      public static final String PARAM_DER_PATH = "path";
58  
59      @Context
60      ResourceInfo resourceInfo;
61  
62      /**
63       * checks if the given REST API operation is allowed
64       * @param permission "read" or "write"
65       * @param path - the REST API path, e.g. /v1/messages
66       *
67       * @throws jakarta.ws.rs.ForbiddenException if access is restricted
68       */
69      private void checkRestAPIAccess(final ContainerRequestContext requestContext,
70          final MCRRestAPIACLPermission permission, final String path) throws ForbiddenException {
71          LogManager.getLogger().warn(path + ": Checking API access: " + permission);
72          final MCRRequestScopeACL aclProvider = MCRRequestScopeACL.getInstance(requestContext);
73          if (MCRRestAccessManager.checkRestAPIAccess(aclProvider, permission, path)) {
74              return;
75          }
76          throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
77              .withErrorCode(MCRErrorCodeConstants.API_NO_PERMISSION)
78              .withMessage("REST-API action is not allowed.")
79              .withDetail("Check access right '" + permission + "' on ACLs 'restapi:/' and 'restapi:" + path + "'!")
80              .toException();
81      }
82  
83      private void checkBaseAccess(ContainerRequestContext requestContext, MCRRestAPIACLPermission permission,
84          String objectId, String derId, String path)
85          throws ForbiddenException {
86          LogManager.getLogger().debug("Permission: {}, Object: {}, Derivate: {}, Path: {}", permission, objectId, derId,
87              path);
88          Optional<String> checkable = Optional.ofNullable(derId)
89              .filter(d -> path != null) //only check for derId if path is given
90              .map(Optional::of)
91              .orElseGet(() -> Optional.ofNullable(objectId));
92          checkable.ifPresent(id -> LogManager.getLogger().info("Checking " + permission + " access on " + id));
93          MCRRequestScopeACL aclProvider = MCRRequestScopeACL.getInstance(requestContext);
94          boolean allowed = checkable
95              .map(id -> aclProvider.checkPermission(id, permission.toString()))
96              .orElse(true);
97          if (allowed) {
98              return;
99          }
100         if (checkable.get().equals(objectId)) {
101             throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
102                 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NO_PERMISSION)
103                 .withMessage("You do not have " + permission + " permission on MCRObject " + objectId + ".")
104                 .toException();
105         }
106         throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
107             .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NO_PERMISSION)
108             .withMessage("You do not have " + permission + " permission on MCRDerivate " + derId + ".")
109             .toException();
110     }
111 
112     private void checkDetailLevel(ContainerRequestContext requestContext, String... detail) throws ForbiddenException {
113         MCRRequestScopeACL aclProvider = MCRRequestScopeACL.getInstance(requestContext);
114         List<String> missedPermissions = Stream.of(detail)
115             .map(d -> "rest-detail-" + d)
116             .filter(d -> MCRAccessManager.hasRule(MCRAccessControlSystem.POOL_PRIVILEGE_ID, d))
117             .filter(d -> !aclProvider.checkPermission(d))
118             .collect(Collectors.toList());
119         if (!missedPermissions.isEmpty()) {
120             throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
121                 .withErrorCode(MCRErrorCodeConstants.API_NO_PERMISSION)
122                 .withMessage("REST-API action is not allowed.")
123                 .withDetail("Check access right(s) '" + missedPermissions + "' on "
124                     + MCRAccessControlSystem.POOL_PRIVILEGE_ID + "'!")
125                 .toException();
126         }
127     }
128 
129     @Override
130     public void filter(ContainerRequestContext requestContext) {
131         final String method = requestContext.getMethod();
132         if (HttpMethod.OPTIONS.equals(method)) {
133             return;
134         }
135         final MCRRestRequiredPermission annotation
136             = resourceInfo.getResourceMethod().getAnnotation(MCRRestRequiredPermission.class);
137         final MCRRestAPIACLPermission permission = Optional.ofNullable(annotation)
138             .map(a -> a.value())
139             .orElseGet(() -> MCRRestAPIACLPermission.fromMethod(method));
140         Optional.ofNullable(resourceInfo.getResourceClass().getAnnotation(Path.class))
141             .map(Path::value)
142             .ifPresent(path -> {
143                 checkRestAPIAccess(requestContext, permission, path);
144                 MultivaluedMap<String, String> pathParameters = requestContext.getUriInfo().getPathParameters();
145                 checkBaseAccess(requestContext, permission, pathParameters.getFirst(PARAM_MCRID),
146                     pathParameters.getFirst(PARAM_DERID), pathParameters.getFirst(PARAM_DER_PATH));
147             });
148         checkDetailLevel(requestContext,
149             requestContext.getAcceptableMediaTypes()
150                 .stream()
151                 .map(m -> m.getParameters().get(MCRDetailLevel.MEDIA_TYPE_PARAMETER))
152                 .filter(Objects::nonNull)
153                 .toArray(String[]::new));
154     }
155 }