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 return resource != null && Stream.of(resource.getInteractionModel()).map(URI::create) 572 .anyMatch(directOrIndirect::contains); 573 } 574 575 /** 576 * Check if we are authorized to access the target of membershipRelation if required. Really this is a test for 577 * failure. The default is true because we might not be looking at an indirect or direct container. 578 * 579 * @param request The current request 580 * @param currentUser The current principal 581 * @param resource The resource 582 * @param container The container 583 * @return Whether we are creating an indirect/direct container and can write the membershipRelation 584 * @throws IOException when getting request's inputstream 585 */ 586 private boolean isAuthorizedForMembershipResource(final HttpServletRequest request, final Subject currentUser, 587 final FedoraResource resource, final FedoraResource container) 588 throws IOException { 589 if (resource != null && request.getMethod().equalsIgnoreCase("POST")) { 590 // Check resource if it exists and we are POSTing to it. 591 if (isResourceIndirectOrDirect(resource)) { 592 final URI membershipResource = getHasMemberFromResource(request); 593 addURIToAuthorize(request, membershipResource); 594 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 595 return false; 596 } 597 } 598 } else if (request.getMethod().equalsIgnoreCase("PUT")) { 599 // PUT to a URI check that the immediate container is not direct or indirect. 600 if (isResourceIndirectOrDirect(container)) { 601 final URI membershipResource = getHasMemberFromResource(request, container); 602 addURIToAuthorize(request, membershipResource); 603 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 604 return false; 605 } 606 } 607 } else if (isSparqlUpdate(request) && isResourceIndirectOrDirect(resource)) { 608 // PATCH to a direct/indirect might change the ldp:membershipResource 609 final URI membershipResource = getHasMemberFromPatch(request); 610 if (membershipResource != null) { 611 log.debug("Found membership resource: {}", membershipResource); 612 // add the membership URI to the list URIs to retrieve ACLs for 613 addURIToAuthorize(request, membershipResource); 614 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 615 return false; 616 } 617 } 618 } else if (request.getMethod().equalsIgnoreCase("DELETE")) { 619 if (isResourceIndirectOrDirect(resource)) { 620 // If we delete a direct/indirect container we have to have access to the ldp:membershipResource 621 final URI membershipResource = getHasMemberFromResource(request); 622 addURIToAuthorize(request, membershipResource); 623 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 624 return false; 625 } 626 } else if (isResourceIndirectOrDirect(container)) { 627 // or if we delete a child of a direct/indirect container we have to have access to the 628 // ldp:membershipResource 629 final URI membershipResource = getHasMemberFromResource(request, container); 630 addURIToAuthorize(request, membershipResource); 631 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 632 return false; 633 } 634 } 635 } 636 637 if (isPayloadIndirectOrDirect(request)) { 638 // Check if we are creating a direct/indirect container. 639 final URI membershipResource = getHasMemberFromRequest(request); 640 if (membershipResource != null) { 641 log.debug("Found membership resource: {}", membershipResource); 642 // add the membership URI to the list URIs to retrieve ACLs for 643 addURIToAuthorize(request, membershipResource); 644 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 645 return false; 646 } 647 } 648 } 649 // Not indirect/directs or we are authorized. 650 return true; 651 } 652 653 /** 654 * Get the memberRelation object from the contents. 655 * 656 * @param request The request. 657 * @return The URI of the memberRelation object 658 * @throws IOException when getting request's inputstream 659 */ 660 private URI getHasMemberFromRequest(final HttpServletRequest request) throws IOException { 661 final String baseUri = request.getRequestURL().toString(); 662 final RDFReaderI reader; 663 final String contentType = request.getContentType(); 664 final Lang format = contentTypeToLang(contentType); 665 final Model inputModel; 666 try { 667 inputModel = createDefaultModel(); 668 reader = inputModel.getReader(format.getName().toUpperCase()); 669 reader.read(inputModel, request.getInputStream(), baseUri); 670 final Statement st = inputModel.getProperty(null, MEMBERSHIP_RESOURCE); 671 return (st != null ? URI.create(st.getObject().toString()) : null); 672 } catch (final RiotException e) { 673 throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e); 674 } catch (final RuntimeIOException e) { 675 if (e.getCause() instanceof JsonParseException) { 676 final var cause = e.getCause(); 677 throw new MalformedRdfException(cause.getMessage(), cause); 678 } 679 throw new RepositoryRuntimeException(e.getMessage(), e); 680 } 681 } 682 683 /** 684 * Get the membershipRelation from a PATCH request 685 * 686 * @param request the http request 687 * @return URI of the first ldp:membershipRelation object. 688 * @throws IOException converting the request body to a string. 689 */ 690 private URI getHasMemberFromPatch(final HttpServletRequest request) throws IOException { 691 final String sparqlString = IOUtils.toString(request.getInputStream(), UTF_8); 692 final String baseURI = request.getRequestURL().toString().replace(request.getContextPath(), "").replaceAll( 693 request.getPathInfo(), "").replaceAll("rest$", ""); 694 final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString); 695 // The INSERT|DELETE DATA quads 696 final Stream<Quad> insertDeleteData = sparqlUpdate.getOperations().stream() 697 .filter(update -> update instanceof UpdateData) 698 .map(update -> (UpdateData) update) 699 .flatMap(update -> update.getQuads().stream()); 700 // Get the UpdateModify instance to re-use below. 701 final List<UpdateModify> updateModifyStream = sparqlUpdate.getOperations().stream() 702 .filter(update -> (update instanceof UpdateModify)) 703 .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString())) 704 .map(update -> (UpdateModify) update) 705 .collect(toList()); 706 // The INSERT {} WHERE {} quads 707 final Stream<Quad> insertQuadData = updateModifyStream.stream() 708 .flatMap(update -> update.getInsertQuads().stream()); 709 // The DELETE {} WHERE {} quads 710 final Stream<Quad> deleteQuadData = updateModifyStream.stream() 711 .flatMap(update -> update.getDeleteQuads().stream()); 712 // The ldp:membershipResource triples. 713 return Stream.concat(Stream.concat(insertDeleteData, insertQuadData), deleteQuadData) 714 .filter(update -> update.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && update.getObject() 715 .isURI()) 716 .map(update -> update.getObject().getURI()) 717 .map(update -> update.replace("file:///", baseURI)) 718 .findFirst().map(URI::create).orElse(null); 719 } 720 721 /** 722 * Get ldp:membershipResource from an existing resource 723 * 724 * @param request the request 725 * @return URI of the ldp:membershipResource triple or null if not found. 726 */ 727 private URI getHasMemberFromResource(final HttpServletRequest request) { 728 final FedoraResource resource = resource(request); 729 return getHasMemberFromResource(request, resource); 730 } 731 732 /** 733 * Get ldp:membershipResource from an existing resource 734 * 735 * @param request the request 736 * @param resource the FedoraResource 737 * @return URI of the ldp:membershipResource triple or null if not found. 738 */ 739 private URI getHasMemberFromResource(final HttpServletRequest request, final FedoraResource resource) { 740 return resource.getTriples() 741 .filter(triple -> triple.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && triple.getObject() 742 .isURI()) 743 .map(Triple::getObject).map(Node::getURI) 744 .findFirst().map(URI::create).orElse(null); 745 } 746 747 /** 748 * Determine if the resource is a binary or a binary description. 749 * @param resource the fedora resource to check 750 * @return true if a binary or binary description. 751 */ 752 private static boolean isBinaryOrDescription(final FedoraResource resource) { 753 return resource.getInteractionModel().equals(NON_RDF_SOURCE.toString()) || 754 resource.getInteractionModel().equals(FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI); 755 } 756 757 /** 758 * Determine if the request is for embedding container resource descriptions. 759 * @param request the request 760 * @return true if include the Prefer tag for http://www.w3.org/ns/oa#PreferContainedDescriptions 761 */ 762 private static boolean isEmbeddedRequest(final HttpServletRequest request) { 763 final var preferTags = request.getHeaders("Prefer"); 764 final Set<SinglePrefer> preferTagSet = new HashSet<>(); 765 while (preferTags.hasMoreElements()) { 766 preferTagSet.add(new SinglePrefer(preferTags.nextElement())); 767 } 768 final MultiPrefer multiPrefer = new MultiPrefer(preferTagSet); 769 if (multiPrefer.hasReturn()) { 770 final LdpPreferTag ldpPreferences = new LdpPreferTag(multiPrefer.getReturn()); 771 return ldpPreferences.displayEmbed(); 772 } 773 return false; 774 } 775 776 /** 777 * Is the user authorized to access the immediately contained resources of the requested resource. 778 * @param request the request 779 * @param currentUser the current user 780 * @param resource the resource being requested. 781 * @return true if authorized or not an embedded resource request on a container. 782 */ 783 private boolean isAuthorizedForEmbeddedRequest(final HttpServletRequest request, final Subject currentUser, 784 final FedoraResource resource) { 785 if (isEmbeddedRequest(request)) { 786 return isAuthorizedForContainedResources(resource, WEBAC_MODE_READ, request, currentUser, false); 787 } 788 // Is not an embedded resource request 789 return true; 790 } 791 792 /** 793 * Utility to check for a permission on the contained resources of a parent resource. 794 * @param resource the parent resource 795 * @param permission the permission required 796 * @param request the current request 797 * @param currentUser the current user 798 * @param deepTraversal whether to check children of children. 799 * @return true if we are allowed access to all descendants, false otherwise. 800 */ 801 private boolean isAuthorizedForContainedResources(final FedoraResource resource, final URI permission, 802 final HttpServletRequest request, final Subject currentUser, 803 final boolean deepTraversal) { 804 if (!isBinaryOrDescription(resource)) { 805 final Transaction transaction = transaction(request); 806 final Stream<FedoraResource> children = resourceFactory.getChildren(transaction, resource.getFedoraId()); 807 return children.noneMatch(resc -> { 808 final URI childURI = URI.create(resc.getFedoraId().getFullId()); 809 log.debug("Found embedded resource: {}", resc); 810 // add the contained URI to the list URIs to retrieve ACLs for 811 addURIToAuthorize(request, childURI); 812 if (!currentUser.isPermitted(new WebACPermission(permission, childURI))) { 813 log.debug("Failed to access embedded resource: {}", childURI); 814 return true; 815 } 816 if (deepTraversal) { 817 // We invert this because the recursive noneMatch reports opposite what we want in here. 818 // Here we want the true (no children failed) to become a false (no children matched a failure). 819 return !isAuthorizedForContainedResources(resc, permission, request, currentUser, deepTraversal); 820 } 821 return false; 822 }); 823 } 824 // Is a binary or description. 825 return true; 826 } 827 828}