001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006 007package org.fcrepo.auth.webac; 008 009import static java.nio.charset.StandardCharsets.UTF_8; 010import static java.util.stream.Collectors.toList; 011import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; 012import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; 013import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; 014import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 015import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; 016import static org.apache.jena.riot.WebContent.contentTypeJSONLD; 017import static org.apache.jena.riot.WebContent.contentTypeN3; 018import static org.apache.jena.riot.WebContent.contentTypeNTriples; 019import static org.apache.jena.riot.WebContent.contentTypeRDFXML; 020import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate; 021import static org.apache.jena.riot.WebContent.contentTypeTurtle; 022import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_ADMIN_ROLE; 023import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_USER_ROLE; 024import static org.fcrepo.auth.webac.URIConstants.FOAF_AGENT_VALUE; 025import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_APPEND; 026import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_CONTROL; 027import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_READ; 028import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_WRITE; 029import static org.fcrepo.auth.webac.WebACAuthorizingRealm.URIS_TO_AUTHORIZE; 030import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET; 031import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER; 032import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; 033import static org.fcrepo.kernel.api.FedoraTypes.FCR_TX; 034import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER; 035import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI; 036import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER; 037import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE; 038import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; 039import static org.slf4j.LoggerFactory.getLogger; 040 041import java.io.IOException; 042import java.net.URI; 043import java.security.Principal; 044import java.util.Collections; 045import java.util.HashSet; 046import java.util.List; 047import java.util.Set; 048import java.util.stream.Collectors; 049import java.util.stream.Stream; 050 051import javax.inject.Inject; 052import javax.servlet.FilterChain; 053import javax.servlet.ServletException; 054import javax.servlet.http.HttpServletRequest; 055import javax.servlet.http.HttpServletResponse; 056import javax.ws.rs.BadRequestException; 057import javax.ws.rs.core.Link; 058import javax.ws.rs.core.MediaType; 059import javax.ws.rs.core.UriBuilder; 060 061import org.fcrepo.config.FedoraPropsConfig; 062import org.fcrepo.http.commons.api.rdf.HttpIdentifierConverter; 063import org.fcrepo.http.commons.domain.MultiPrefer; 064import org.fcrepo.http.commons.domain.SinglePrefer; 065import org.fcrepo.http.commons.domain.ldp.LdpPreferTag; 066import org.fcrepo.http.commons.session.TransactionProvider; 067import org.fcrepo.kernel.api.ReadOnlyTransaction; 068import org.fcrepo.kernel.api.Transaction; 069import org.fcrepo.kernel.api.TransactionManager; 070import org.fcrepo.kernel.api.exception.InvalidResourceIdentifierException; 071import org.fcrepo.kernel.api.exception.MalformedRdfException; 072import org.fcrepo.kernel.api.exception.PathNotFoundException; 073import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 074import org.fcrepo.kernel.api.exception.TransactionRuntimeException; 075import org.fcrepo.kernel.api.identifiers.FedoraId; 076import org.fcrepo.kernel.api.models.FedoraResource; 077import org.fcrepo.kernel.api.models.ResourceFactory; 078 079import org.apache.commons.io.IOUtils; 080import org.apache.jena.atlas.RuntimeIOException; 081import org.apache.jena.graph.Node; 082import org.apache.jena.graph.Triple; 083import org.apache.jena.query.QueryParseException; 084import org.apache.jena.rdf.model.Model; 085import org.apache.jena.rdf.model.RDFReaderI; 086import org.apache.jena.rdf.model.Resource; 087import org.apache.jena.rdf.model.Statement; 088import org.apache.jena.riot.Lang; 089import org.apache.jena.riot.RiotException; 090import org.apache.jena.sparql.core.Quad; 091import org.apache.jena.sparql.modify.request.UpdateData; 092import org.apache.jena.sparql.modify.request.UpdateDataDelete; 093import org.apache.jena.sparql.modify.request.UpdateModify; 094import org.apache.jena.update.UpdateFactory; 095import org.apache.jena.update.UpdateRequest; 096import org.apache.shiro.SecurityUtils; 097import org.apache.shiro.subject.PrincipalCollection; 098import org.apache.shiro.subject.SimplePrincipalCollection; 099import org.apache.shiro.subject.Subject; 100import org.slf4j.Logger; 101import org.springframework.web.filter.RequestContextFilter; 102 103import com.fasterxml.jackson.core.JsonParseException; 104 105/** 106 * @author peichman 107 */ 108public class WebACFilter extends RequestContextFilter { 109 110 private static final Logger log = getLogger(WebACFilter.class); 111 112 private static final MediaType sparqlUpdate = MediaType.valueOf(contentTypeSPARQLUpdate); 113 114 private static final Principal FOAF_AGENT_PRINCIPAL = new Principal() { 115 116 @Override 117 public String getName() { 118 return FOAF_AGENT_VALUE; 119 } 120 121 @Override 122 public String toString() { 123 return getName(); 124 } 125 126 }; 127 128 private static final PrincipalCollection FOAF_AGENT_PRINCIPAL_COLLECTION = 129 new SimplePrincipalCollection(FOAF_AGENT_PRINCIPAL, WebACAuthorizingRealm.class.getCanonicalName()); 130 131 private static Subject FOAF_AGENT_SUBJECT; 132 133 @Inject 134 private FedoraPropsConfig fedoraPropsConfig; 135 136 @Inject 137 private ResourceFactory resourceFactory; 138 139 @Inject 140 private TransactionManager transactionManager; 141 142 private static Set<URI> directOrIndirect = Set.of(INDIRECT_CONTAINER, DIRECT_CONTAINER).stream() 143 .map(Resource::toString).map(URI::create).collect(Collectors.toSet()); 144 145 private static Set<String> rdfContentTypes = Set.of(contentTypeTurtle, contentTypeJSONLD, contentTypeN3, 146 contentTypeRDFXML, contentTypeNTriples); 147 148 /** 149 * Generate a HttpIdentifierConverter from the request URL. 150 * @param request the servlet request. 151 * @return a converter. 152 */ 153 public static HttpIdentifierConverter identifierConverter(final HttpServletRequest request) { 154 final var uriBuild = UriBuilder.fromUri(getBaseUri(request)).path("/{path: .*}"); 155 return new HttpIdentifierConverter(uriBuild); 156 } 157 158 /** 159 * Calculate a base Uri for this request. 160 * @param request the incoming request 161 * @return the URI 162 */ 163 public static URI getBaseUri(final HttpServletRequest request) { 164 final String host = request.getScheme() + "://" + request.getServerName() + 165 (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + "/"; 166 final String requestUrl = request.getRequestURL().toString(); 167 final String contextPath = request.getContextPath() + request.getServletPath(); 168 final String baseUri; 169 if (contextPath.length() == 0) { 170 baseUri = host; 171 } else { 172 baseUri = requestUrl.split(contextPath)[0] + contextPath + "/"; 173 } 174 return URI.create(baseUri); 175 } 176 177 /** 178 * Add URIs to collect permissions information for. 179 * 180 * @param httpRequest the request. 181 * @param uri the uri to check. 182 */ 183 private void addURIToAuthorize(final HttpServletRequest httpRequest, final URI uri) { 184 @SuppressWarnings("unchecked") 185 Set<URI> targetURIs = (Set<URI>) httpRequest.getAttribute(URIS_TO_AUTHORIZE); 186 if (targetURIs == null) { 187 targetURIs = new HashSet<>(); 188 httpRequest.setAttribute(URIS_TO_AUTHORIZE, targetURIs); 189 } 190 targetURIs.add(uri); 191 } 192 193 @Override 194 protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, 195 final FilterChain chain) throws ServletException, IOException { 196 197 // Ensure we are not trying to operate on a closed or invalid transaction. 198 try { 199 transaction(request); 200 } catch (final TransactionRuntimeException e) { 201 printException(response, SC_CONFLICT, e); 202 return; 203 } 204 final Subject currentUser = SecurityUtils.getSubject(); 205 HttpServletRequest httpRequest = request; 206 if (isSparqlUpdate(httpRequest) || isRdfRequest(httpRequest)) { 207 // If this is a sparql request or contains RDF. 208 httpRequest = new CachedHttpRequest(httpRequest); 209 } 210 211 final String requestUrl = httpRequest.getRequestURL().toString(); 212 try { 213 FedoraId.create(identifierConverter(httpRequest).toInternalId(requestUrl)); 214 } catch (final InvalidResourceIdentifierException e) { 215 printException(response, SC_BAD_REQUEST, e); 216 return; 217 } catch (final IllegalArgumentException e) { 218 // No Fedora request path provided, so just continue along. 219 } 220 221 // add the request URI to the list of URIs to retrieve the ACLs for 222 addURIToAuthorize(httpRequest, URI.create(requestUrl)); 223 224 if (currentUser.isAuthenticated()) { 225 log.debug("User is authenticated"); 226 if (currentUser.hasRole(FEDORA_ADMIN_ROLE)) { 227 log.debug("User has fedoraAdmin role"); 228 } else if (currentUser.hasRole(FEDORA_USER_ROLE)) { 229 log.debug("User has fedoraUser role"); 230 // non-admins are subject to permission checks 231 if (!isAuthorized(currentUser, httpRequest)) { 232 // if the user is not authorized, set response to forbidden 233 response.sendError(SC_FORBIDDEN); 234 return; 235 } 236 } else { 237 log.debug("User has no recognized servlet container role"); 238 // missing a container role, return forbidden 239 response.sendError(SC_FORBIDDEN); 240 return; 241 } 242 } else { 243 log.debug("User is NOT authenticated"); 244 // anonymous users are subject to permission checks 245 if (!isAuthorized(getFoafAgentSubject(), httpRequest)) { 246 // if anonymous user is not authorized, set response to forbidden 247 response.sendError(SC_FORBIDDEN); 248 return; 249 } 250 } 251 252 // proceed to the next filter 253 chain.doFilter(httpRequest, response); 254 } 255 256 /** 257 * Displays the message from the exception to the screen. 258 * @param response the servlet response 259 * @param e the exception being handled 260 * @throws IOException if problems opening the output writer. 261 */ 262 private void printException(final HttpServletResponse response, final int responseCode, final Throwable e) 263 throws IOException { 264 final var message = e.getMessage(); 265 response.resetBuffer(); 266 response.setStatus(responseCode); 267 response.setContentType(TEXT_PLAIN_WITH_CHARSET); 268 response.setContentLength(message.length()); 269 response.setCharacterEncoding("UTF-8"); 270 final var write = response.getWriter(); 271 write.write(message); 272 write.flush(); 273 } 274 275 private Subject getFoafAgentSubject() { 276 if (FOAF_AGENT_SUBJECT == null) { 277 FOAF_AGENT_SUBJECT = new Subject.Builder().principals(FOAF_AGENT_PRINCIPAL_COLLECTION).buildSubject(); 278 } 279 return FOAF_AGENT_SUBJECT; 280 } 281 282 private Transaction transaction(final HttpServletRequest request) { 283 final String txId = request.getHeader(ATOMIC_ID_HEADER); 284 if (txId == null) { 285 return ReadOnlyTransaction.INSTANCE; 286 } 287 final var txProvider = new TransactionProvider(transactionManager, request, 288 getBaseUri(request), fedoraPropsConfig.getJmsBaseUrl()); 289 return txProvider.provide(); 290 } 291 292 private String getContainerUrl(final HttpServletRequest servletRequest) { 293 final String pathInfo = servletRequest.getPathInfo(); 294 final String baseUrl = servletRequest.getRequestURL().toString().replace(pathInfo, ""); 295 final String[] paths = pathInfo.split("/"); 296 final String[] parentPaths = java.util.Arrays.copyOfRange(paths, 0, paths.length - 1); 297 return baseUrl + String.join("/", parentPaths); 298 } 299 300 private FedoraResource getContainer(final HttpServletRequest servletRequest) { 301 final FedoraResource resource = resource(servletRequest); 302 if (resource != null) { 303 return resource(servletRequest).getContainer(); 304 } 305 final String parentURI = getContainerUrl(servletRequest); 306 return resource(servletRequest, getIdFromRequest(servletRequest, parentURI)); 307 } 308 309 private FedoraResource resource(final HttpServletRequest servletRequest) { 310 return resource(servletRequest, getIdFromRequest(servletRequest)); 311 } 312 313 private FedoraResource resource(final HttpServletRequest servletRequest, final FedoraId resourceId) { 314 try { 315 return this.resourceFactory.getResource(transaction(servletRequest), resourceId); 316 } catch (final PathNotFoundException e) { 317 return null; 318 } 319 } 320 321 private FedoraId getIdFromRequest(final HttpServletRequest servletRequest) { 322 final String httpURI = servletRequest.getRequestURL().toString(); 323 return getIdFromRequest(servletRequest, httpURI); 324 } 325 326 private FedoraId getIdFromRequest(final HttpServletRequest request, final String httpURI) { 327 return FedoraId.create(identifierConverter(request).toInternalId(httpURI)); 328 } 329 330 private boolean isAuthorized(final Subject currentUser, final HttpServletRequest httpRequest) throws IOException { 331 final String requestURL = httpRequest.getRequestURL().toString(); 332 final boolean isAcl = requestURL.endsWith(FCR_ACL); 333 final boolean isTxEndpoint = requestURL.endsWith(FCR_TX) || requestURL.endsWith(FCR_TX + "/"); 334 final URI requestURI = URI.create(requestURL); 335 log.debug("Request URI is {}", requestURI); 336 final FedoraResource resource = resource(httpRequest); 337 final FedoraResource container = getContainer(httpRequest); 338 339 // WebAC permissions 340 final WebACPermission toRead = new WebACPermission(WEBAC_MODE_READ, requestURI); 341 final WebACPermission toWrite = new WebACPermission(WEBAC_MODE_WRITE, requestURI); 342 final WebACPermission toAppend = new WebACPermission(WEBAC_MODE_APPEND, requestURI); 343 final WebACPermission toControl = new WebACPermission(WEBAC_MODE_CONTROL, requestURI); 344 345 switch (httpRequest.getMethod()) { 346 case "OPTIONS": 347 case "HEAD": 348 case "GET": 349 if (isAcl) { 350 if (currentUser.isPermitted(toControl)) { 351 log.debug("GET allowed by {} permission", toControl); 352 return true; 353 } else { 354 log.debug("GET prohibited without {} permission", toControl); 355 return false; 356 } 357 } else { 358 if (currentUser.isPermitted(toRead)) { 359 if (!isAuthorizedForEmbeddedRequest(httpRequest, currentUser, resource)) { 360 log.debug("GET/HEAD/OPTIONS request to {} denied, user {} not authorized for an embedded " + 361 "resource", requestURL, currentUser.toString()); 362 return false; 363 } 364 return true; 365 } 366 return false; 367 } 368 case "PUT": 369 if (isAcl) { 370 if (currentUser.isPermitted(toControl)) { 371 log.debug("PUT allowed by {} permission", toControl); 372 return true; 373 } else { 374 log.debug("PUT prohibited without {} permission", toControl); 375 return false; 376 } 377 } else if (currentUser.isPermitted(toWrite)) { 378 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 379 log.debug("PUT denied, not authorized to write to membershipRelation"); 380 return false; 381 } 382 log.debug("PUT allowed by {} permission", toWrite); 383 return true; 384 } else { 385 if (resource != null) { 386 // can't PUT to an existing resource without acl:Write permission 387 log.debug("PUT prohibited to existing resource without {} permission", toWrite); 388 return false; 389 } else { 390 // find nearest parent resource and verify that user has acl:Append on it 391 // this works because when the authorizations are inherited, it is the target request URI that is 392 // added as the resource, not the accessTo or other URI in the original authorization 393 log.debug("Resource doesn't exist; checking parent resources for acl:Append permission"); 394 if (currentUser.isPermitted(toAppend)) { 395 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 396 log.debug("PUT denied, not authorized to write to membershipRelation"); 397 return false; 398 } 399 log.debug("PUT allowed for new resource by inherited {} permission", toAppend); 400 return true; 401 } else { 402 log.debug("PUT prohibited for new resource without inherited {} permission", toAppend); 403 return false; 404 } 405 } 406 } 407 case "POST": 408 if (isTxEndpoint && currentUser.isAuthenticated()) { 409 final String currentUsername = ((Principal) currentUser.getPrincipal()).getName(); 410 log.debug("POST allowed to transaction endpoint for authenticated user {}", currentUsername); 411 return true; 412 } 413 if (currentUser.isPermitted(toWrite)) { 414 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 415 log.debug("POST denied, not authorized to write to membershipRelation"); 416 return false; 417 } 418 log.debug("POST allowed by {} permission", toWrite); 419 return true; 420 } 421 if (resource != null) { 422 if (isBinaryOrDescription(resource)) { 423 // LDP-NR 424 // user without the acl:Write permission cannot POST to binaries 425 log.debug("POST prohibited to binary resource without {} permission", toWrite); 426 return false; 427 } else { 428 // LDP-RS 429 // user with the acl:Append permission may POST to containers 430 if (currentUser.isPermitted(toAppend)) { 431 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 432 log.debug("POST denied, not authorized to write to membershipRelation"); 433 return false; 434 } 435 log.debug("POST allowed to container by {} permission", toAppend); 436 return true; 437 } else { 438 log.debug("POST prohibited to container without {} permission", toAppend); 439 return false; 440 } 441 } 442 } else { 443 // prohibit POST to non-existent resources without the acl:Write permission 444 log.debug("POST prohibited to non-existent resource without {} permission", toWrite); 445 return false; 446 } 447 case "DELETE": 448 if (isAcl) { 449 if (currentUser.isPermitted(toControl)) { 450 log.debug("DELETE allowed by {} permission", toControl); 451 return true; 452 } else { 453 log.debug("DELETE prohibited without {} permission", toControl); 454 return false; 455 } 456 } else { 457 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 458 log.debug("DELETE denied, not authorized to write to membershipRelation"); 459 return false; 460 } else if (currentUser.isPermitted(toWrite)) { 461 if (!isAuthorizedForContainedResources(resource, WEBAC_MODE_WRITE, httpRequest, currentUser, 462 true)) { 463 log.debug("DELETE denied, not authorized to write to a descendant of {}", resource); 464 return false; 465 } 466 return true; 467 } 468 return false; 469 } 470 case "PATCH": 471 472 if (isAcl) { 473 if (currentUser.isPermitted(toControl)) { 474 log.debug("PATCH allowed by {} permission", toControl); 475 return true; 476 } else { 477 log.debug("PATCH prohibited without {} permission", toControl); 478 return false; 479 } 480 } else if (currentUser.isPermitted(toWrite)) { 481 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 482 log.debug("PATCH denied, not authorized to write to membershipRelation"); 483 return false; 484 } 485 return true; 486 } else { 487 if (currentUser.isPermitted(toAppend)) { 488 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 489 log.debug("PATCH denied, not authorized to write to membershipRelation"); 490 return false; 491 } 492 return isPatchContentPermitted(httpRequest); 493 } 494 } 495 return false; 496 default: 497 return false; 498 } 499 } 500 501 private boolean isPatchContentPermitted(final HttpServletRequest httpRequest) throws IOException { 502 if (!isSparqlUpdate(httpRequest)) { 503 log.debug("Cannot verify authorization on NON-SPARQL Patch request."); 504 return false; 505 } 506 if (httpRequest.getInputStream() != null) { 507 boolean noDeletes = false; 508 try { 509 noDeletes = !hasDeleteClause(IOUtils.toString(httpRequest.getInputStream(), UTF_8)); 510 } catch (final QueryParseException ex) { 511 log.error("Cannot verify authorization! Exception while inspecting SPARQL query!", ex); 512 } 513 return noDeletes; 514 } else { 515 log.debug("Authorizing SPARQL request with no content."); 516 return true; 517 } 518 } 519 520 private boolean hasDeleteClause(final String sparqlString) { 521 final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString); 522 return sparqlUpdate.getOperations().stream() 523 .filter(update -> update instanceof UpdateDataDelete) 524 .map(update -> (UpdateDataDelete) update) 525 .anyMatch(update -> update.getQuads().size() > 0) || 526 sparqlUpdate.getOperations().stream().filter(update -> (update instanceof UpdateModify)) 527 .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString())) 528 .map(update -> (UpdateModify)update) 529 .filter(UpdateModify::hasDeleteClause) 530 .anyMatch(update -> update.getDeleteQuads().size() > 0); 531 } 532 533 private boolean isSparqlUpdate(final HttpServletRequest request) { 534 try { 535 return request.getMethod().equals("PATCH") && 536 sparqlUpdate.isCompatible(MediaType.valueOf(request 537 .getContentType())); 538 } catch (final IllegalArgumentException e) { 539 return false; 540 } 541 } 542 543 /** 544 * Does the request's content-type match one of the RDF types. 545 * 546 * @param request the http servlet request 547 * @return whether the content-type matches. 548 */ 549 private boolean isRdfRequest(final HttpServletRequest request) { 550 return request.getContentType() != null && rdfContentTypes.contains(request.getContentType()); 551 } 552 553 /** 554 * Is the request to create an indirect or direct container. 555 * 556 * @param request The current request 557 * @return whether we are acting on/creating an indirect/direct container. 558 */ 559 private boolean isPayloadIndirectOrDirect(final HttpServletRequest request) { 560 return Collections.list(request.getHeaders("Link")).stream().map(Link::valueOf).map(Link::getUri) 561 .anyMatch(directOrIndirect::contains); 562 } 563 564 /** 565 * Is the current resource a direct or indirect container 566 * 567 * @param resource the resource to check 568 * @return whether it is a direct or indirect container. 569 */ 570 private boolean isResourceIndirectOrDirect(final FedoraResource resource) { 571 // Tombstone are the only known resource with a null interaction model. 572 return resource != null && resource.getInteractionModel() != null && 573 Stream.of(resource.getInteractionModel()).map(URI::create) 574 .anyMatch(directOrIndirect::contains); 575 } 576 577 /** 578 * Check if we are authorized to access the target of membershipRelation if required. Really this is a test for 579 * failure. The default is true because we might not be looking at an indirect or direct container. 580 * 581 * @param request The current request 582 * @param currentUser The current principal 583 * @param resource The resource 584 * @param container The container 585 * @return Whether we are creating an indirect/direct container and can write the membershipRelation 586 * @throws IOException when getting request's inputstream 587 */ 588 private boolean isAuthorizedForMembershipResource(final HttpServletRequest request, final Subject currentUser, 589 final FedoraResource resource, final FedoraResource container) 590 throws IOException { 591 if (resource != null && request.getMethod().equalsIgnoreCase("POST")) { 592 // Check resource if it exists and we are POSTing to it. 593 if (isResourceIndirectOrDirect(resource)) { 594 final URI membershipResource = getHasMemberFromResource(request); 595 addURIToAuthorize(request, membershipResource); 596 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 597 return false; 598 } 599 } 600 } else if (request.getMethod().equalsIgnoreCase("PUT")) { 601 // PUT to a URI check that the immediate container is not direct or indirect. 602 if (isResourceIndirectOrDirect(container)) { 603 final URI membershipResource = getHasMemberFromResource(request, container); 604 addURIToAuthorize(request, membershipResource); 605 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 606 return false; 607 } 608 } 609 } else if (isSparqlUpdate(request) && isResourceIndirectOrDirect(resource)) { 610 // PATCH to a direct/indirect might change the ldp:membershipResource 611 final URI membershipResource = getHasMemberFromPatch(request); 612 if (membershipResource != null) { 613 log.debug("Found membership resource: {}", membershipResource); 614 // add the membership URI to the list URIs to retrieve ACLs for 615 addURIToAuthorize(request, membershipResource); 616 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 617 return false; 618 } 619 } 620 } else if (request.getMethod().equalsIgnoreCase("DELETE")) { 621 if (isResourceIndirectOrDirect(resource)) { 622 // If we delete a direct/indirect container we have to have access to the ldp:membershipResource 623 final URI membershipResource = getHasMemberFromResource(request); 624 addURIToAuthorize(request, membershipResource); 625 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 626 return false; 627 } 628 } else if (isResourceIndirectOrDirect(container)) { 629 // or if we delete a child of a direct/indirect container we have to have access to the 630 // ldp:membershipResource 631 final URI membershipResource = getHasMemberFromResource(request, container); 632 addURIToAuthorize(request, membershipResource); 633 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 634 return false; 635 } 636 } 637 } 638 639 if (isPayloadIndirectOrDirect(request)) { 640 // Check if we are creating a direct/indirect container. 641 final URI membershipResource = getHasMemberFromRequest(request); 642 if (membershipResource != null) { 643 log.debug("Found membership resource: {}", membershipResource); 644 // add the membership URI to the list URIs to retrieve ACLs for 645 addURIToAuthorize(request, membershipResource); 646 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 647 return false; 648 } 649 } 650 } 651 // Not indirect/directs or we are authorized. 652 return true; 653 } 654 655 /** 656 * Get the memberRelation object from the contents. 657 * 658 * @param request The request. 659 * @return The URI of the memberRelation object 660 * @throws IOException when getting request's inputstream 661 */ 662 private URI getHasMemberFromRequest(final HttpServletRequest request) throws IOException { 663 final String baseUri = request.getRequestURL().toString(); 664 final RDFReaderI reader; 665 final String contentType = request.getContentType(); 666 final Lang format = contentTypeToLang(contentType); 667 final Model inputModel; 668 try { 669 inputModel = createDefaultModel(); 670 reader = inputModel.getReader(format.getName().toUpperCase()); 671 reader.read(inputModel, request.getInputStream(), baseUri); 672 final Statement st = inputModel.getProperty(null, MEMBERSHIP_RESOURCE); 673 return (st != null ? URI.create(st.getObject().toString()) : null); 674 } catch (final RiotException e) { 675 throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e); 676 } catch (final RuntimeIOException e) { 677 if (e.getCause() instanceof JsonParseException) { 678 final var cause = e.getCause(); 679 throw new MalformedRdfException(cause.getMessage(), cause); 680 } 681 throw new RepositoryRuntimeException(e.getMessage(), e); 682 } 683 } 684 685 /** 686 * Get the membershipRelation from a PATCH request 687 * 688 * @param request the http request 689 * @return URI of the first ldp:membershipRelation object. 690 * @throws IOException converting the request body to a string. 691 */ 692 private URI getHasMemberFromPatch(final HttpServletRequest request) throws IOException { 693 final String sparqlString = IOUtils.toString(request.getInputStream(), UTF_8); 694 final String baseURI = request.getRequestURL().toString().replace(request.getContextPath(), "").replaceAll( 695 request.getPathInfo(), "").replaceAll("rest$", ""); 696 final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString); 697 // The INSERT|DELETE DATA quads 698 final Stream<Quad> insertDeleteData = sparqlUpdate.getOperations().stream() 699 .filter(update -> update instanceof UpdateData) 700 .map(update -> (UpdateData) update) 701 .flatMap(update -> update.getQuads().stream()); 702 // Get the UpdateModify instance to re-use below. 703 final List<UpdateModify> updateModifyStream = sparqlUpdate.getOperations().stream() 704 .filter(update -> (update instanceof UpdateModify)) 705 .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString())) 706 .map(update -> (UpdateModify) update) 707 .collect(toList()); 708 // The INSERT {} WHERE {} quads 709 final Stream<Quad> insertQuadData = updateModifyStream.stream() 710 .flatMap(update -> update.getInsertQuads().stream()); 711 // The DELETE {} WHERE {} quads 712 final Stream<Quad> deleteQuadData = updateModifyStream.stream() 713 .flatMap(update -> update.getDeleteQuads().stream()); 714 // The ldp:membershipResource triples. 715 return Stream.concat(Stream.concat(insertDeleteData, insertQuadData), deleteQuadData) 716 .filter(update -> update.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && update.getObject() 717 .isURI()) 718 .map(update -> update.getObject().getURI()) 719 .map(update -> update.replace("file:///", baseURI)) 720 .findFirst().map(URI::create).orElse(null); 721 } 722 723 /** 724 * Get ldp:membershipResource from an existing resource 725 * 726 * @param request the request 727 * @return URI of the ldp:membershipResource triple or null if not found. 728 */ 729 private URI getHasMemberFromResource(final HttpServletRequest request) { 730 final FedoraResource resource = resource(request); 731 return getHasMemberFromResource(request, resource); 732 } 733 734 /** 735 * Get ldp:membershipResource from an existing resource 736 * 737 * @param request the request 738 * @param resource the FedoraResource 739 * @return URI of the ldp:membershipResource triple or null if not found. 740 */ 741 private URI getHasMemberFromResource(final HttpServletRequest request, final FedoraResource resource) { 742 return resource.getTriples() 743 .filter(triple -> triple.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && triple.getObject() 744 .isURI()) 745 .map(Triple::getObject).map(Node::getURI) 746 .findFirst().map(URI::create).orElse(null); 747 } 748 749 /** 750 * Determine if the resource is a binary or a binary description. 751 * @param resource the fedora resource to check 752 * @return true if a binary or binary description. 753 */ 754 private static boolean isBinaryOrDescription(final FedoraResource resource) { 755 // Tombstone are the only known resource with a null interaction model. 756 return resource.getInteractionModel() != null && ( 757 resource.getInteractionModel().equals(NON_RDF_SOURCE.toString()) || 758 resource.getInteractionModel().equals(FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI)); 759 } 760 761 /** 762 * Determine if the request is for embedding container resource descriptions. 763 * @param request the request 764 * @return true if include the Prefer tag for http://www.w3.org/ns/oa#PreferContainedDescriptions 765 */ 766 private static boolean isEmbeddedRequest(final HttpServletRequest request) { 767 final var preferTags = request.getHeaders("Prefer"); 768 final Set<SinglePrefer> preferTagSet = new HashSet<>(); 769 while (preferTags.hasMoreElements()) { 770 preferTagSet.add(new SinglePrefer(preferTags.nextElement())); 771 } 772 final MultiPrefer multiPrefer = new MultiPrefer(preferTagSet); 773 if (multiPrefer.hasReturn()) { 774 final LdpPreferTag ldpPreferences = new LdpPreferTag(multiPrefer.getReturn()); 775 return ldpPreferences.displayEmbed(); 776 } 777 return false; 778 } 779 780 /** 781 * Is the user authorized to access the immediately contained resources of the requested resource. 782 * @param request the request 783 * @param currentUser the current user 784 * @param resource the resource being requested. 785 * @return true if authorized or not an embedded resource request on a container. 786 */ 787 private boolean isAuthorizedForEmbeddedRequest(final HttpServletRequest request, final Subject currentUser, 788 final FedoraResource resource) { 789 if (isEmbeddedRequest(request)) { 790 return isAuthorizedForContainedResources(resource, WEBAC_MODE_READ, request, currentUser, false); 791 } 792 // Is not an embedded resource request 793 return true; 794 } 795 796 /** 797 * Utility to check for a permission on the contained resources of a parent resource. 798 * @param resource the parent resource 799 * @param permission the permission required 800 * @param request the current request 801 * @param currentUser the current user 802 * @param deepTraversal whether to check children of children. 803 * @return true if we are allowed access to all descendants, false otherwise. 804 */ 805 private boolean isAuthorizedForContainedResources(final FedoraResource resource, final URI permission, 806 final HttpServletRequest request, final Subject currentUser, 807 final boolean deepTraversal) { 808 if (!isBinaryOrDescription(resource)) { 809 final Transaction transaction = transaction(request); 810 final Stream<FedoraResource> children = resourceFactory.getChildren(transaction, resource.getFedoraId()); 811 return children.noneMatch(resc -> { 812 final URI childURI = URI.create(resc.getFedoraId().getFullId()); 813 log.debug("Found embedded resource: {}", resc); 814 // add the contained URI to the list URIs to retrieve ACLs for 815 addURIToAuthorize(request, childURI); 816 if (!currentUser.isPermitted(new WebACPermission(permission, childURI))) { 817 log.debug("Failed to access embedded resource: {}", childURI); 818 return true; 819 } 820 if (deepTraversal) { 821 // We invert this because the recursive noneMatch reports opposite what we want in here. 822 // Here we want the true (no children failed) to become a false (no children matched a failure). 823 return !isAuthorizedForContainedResources(resc, permission, request, currentUser, deepTraversal); 824 } 825 return false; 826 }); 827 } 828 // Is a binary or description. 829 return true; 830 } 831 832}