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