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