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