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.session.TransactionProvider; 045import org.fcrepo.kernel.api.Transaction; 046import org.fcrepo.kernel.api.TransactionManager; 047import org.fcrepo.kernel.api.exception.InvalidResourceIdentifierException; 048import org.fcrepo.kernel.api.exception.MalformedRdfException; 049import org.fcrepo.kernel.api.exception.PathNotFoundException; 050import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 051import org.fcrepo.kernel.api.identifiers.FedoraId; 052import org.fcrepo.kernel.api.models.FedoraResource; 053import org.fcrepo.kernel.api.models.ResourceFactory; 054import org.slf4j.Logger; 055import org.springframework.web.filter.RequestContextFilter; 056 057import javax.inject.Inject; 058import javax.servlet.FilterChain; 059import javax.servlet.ServletException; 060import javax.servlet.http.HttpServletRequest; 061import javax.servlet.http.HttpServletResponse; 062import javax.ws.rs.BadRequestException; 063import javax.ws.rs.core.Link; 064import javax.ws.rs.core.MediaType; 065import javax.ws.rs.core.UriBuilder; 066import java.io.IOException; 067import java.net.URI; 068import java.security.Principal; 069import java.util.Collections; 070import java.util.HashSet; 071import java.util.List; 072import java.util.Set; 073import java.util.stream.Collectors; 074import java.util.stream.Stream; 075 076import static java.nio.charset.StandardCharsets.UTF_8; 077import static java.util.stream.Collectors.toList; 078import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; 079import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; 080import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 081import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; 082import static org.apache.jena.riot.WebContent.contentTypeJSONLD; 083import static org.apache.jena.riot.WebContent.contentTypeN3; 084import static org.apache.jena.riot.WebContent.contentTypeNTriples; 085import static org.apache.jena.riot.WebContent.contentTypeRDFXML; 086import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate; 087import static org.apache.jena.riot.WebContent.contentTypeTurtle; 088import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_ADMIN_ROLE; 089import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_USER_ROLE; 090import static org.fcrepo.auth.webac.URIConstants.FOAF_AGENT_VALUE; 091import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_APPEND; 092import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_CONTROL; 093import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_READ; 094import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_WRITE; 095import static org.fcrepo.auth.webac.WebACAuthorizingRealm.URIS_TO_AUTHORIZE; 096import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER; 097import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; 098import static org.fcrepo.kernel.api.FedoraTypes.FCR_TX; 099import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER; 100import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI; 101import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER; 102import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE; 103import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; 104import static org.slf4j.LoggerFactory.getLogger; 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 ResourceFactory resourceFactory; 136 137 @Inject 138 private TransactionManager transactionManager; 139 140 private static Set<URI> directOrIndirect = Set.of(INDIRECT_CONTAINER, DIRECT_CONTAINER).stream() 141 .map(Resource::toString).map(URI::create).collect(Collectors.toSet()); 142 143 private static Set<String> rdfContentTypes = Set.of(contentTypeTurtle, contentTypeJSONLD, contentTypeN3, 144 contentTypeRDFXML, contentTypeNTriples); 145 146 /** 147 * Generate a HttpIdentifierConverter from the request URL. 148 * @param request the servlet request. 149 * @return a converter. 150 */ 151 public static HttpIdentifierConverter identifierConverter(final HttpServletRequest request) { 152 final var uriBuild = UriBuilder.fromUri(getBaseUri(request)).path("/{path: .*}"); 153 return new HttpIdentifierConverter(uriBuild); 154 } 155 156 /** 157 * Calculate a base Uri for this request. 158 * @param request the incoming request 159 * @return the URI 160 */ 161 public static URI getBaseUri(final HttpServletRequest request) { 162 final String host = request.getScheme() + "://" + request.getServerName() + 163 (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + "/"; 164 final String requestUrl = request.getRequestURL().toString(); 165 final String contextPath = request.getContextPath() + request.getServletPath(); 166 final String baseUri; 167 if (contextPath.length() == 0) { 168 baseUri = host; 169 } else { 170 baseUri = requestUrl.split(contextPath)[0] + contextPath + "/"; 171 } 172 return URI.create(baseUri); 173 } 174 175 /** 176 * Add URIs to collect permissions information for. 177 * 178 * @param httpRequest the request. 179 * @param uri the uri to check. 180 */ 181 private void addURIToAuthorize(final HttpServletRequest httpRequest, final URI uri) { 182 @SuppressWarnings("unchecked") 183 Set<URI> targetURIs = (Set<URI>) httpRequest.getAttribute(URIS_TO_AUTHORIZE); 184 if (targetURIs == null) { 185 targetURIs = new HashSet<>(); 186 httpRequest.setAttribute(URIS_TO_AUTHORIZE, targetURIs); 187 } 188 targetURIs.add(uri); 189 } 190 191 @Override 192 protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, 193 final FilterChain chain) throws ServletException, IOException { 194 final Subject currentUser = SecurityUtils.getSubject(); 195 HttpServletRequest httpRequest = request; 196 if (isSparqlUpdate(httpRequest) || isRdfRequest(httpRequest)) { 197 // If this is a sparql request or contains RDF. 198 httpRequest = new CachedHttpRequest(httpRequest); 199 } 200 201 final String requestUrl = httpRequest.getRequestURL().toString(); 202 try { 203 FedoraId.create(identifierConverter(httpRequest).toInternalId(requestUrl)); 204 } catch (final InvalidResourceIdentifierException e) { 205 response.sendError(SC_BAD_REQUEST, 206 String.format("Path contains empty element! %s", httpRequest.getRequestURI())); 207 } catch (final IllegalArgumentException e) { 208 // No Fedora request path provided, so just continue along. 209 } 210 211 // add the request URI to the list of URIs to retrieve the ACLs for 212 addURIToAuthorize(httpRequest, URI.create(requestUrl)); 213 214 if (currentUser.isAuthenticated()) { 215 log.debug("User is authenticated"); 216 if (currentUser.hasRole(FEDORA_ADMIN_ROLE)) { 217 log.debug("User has fedoraAdmin role"); 218 } else if (currentUser.hasRole(FEDORA_USER_ROLE)) { 219 log.debug("User has fedoraUser role"); 220 // non-admins are subject to permission checks 221 if (!isAuthorized(currentUser, httpRequest)) { 222 // if the user is not authorized, set response to forbidden 223 response.sendError(SC_FORBIDDEN); 224 return; 225 } 226 } else { 227 log.debug("User has no recognized servlet container role"); 228 // missing a container role, return forbidden 229 response.sendError(SC_FORBIDDEN); 230 return; 231 } 232 } else { 233 log.debug("User is NOT authenticated"); 234 // anonymous users are subject to permission checks 235 if (!isAuthorized(getFoafAgentSubject(), httpRequest)) { 236 // if anonymous user is not authorized, set response to forbidden 237 response.sendError(SC_FORBIDDEN); 238 return; 239 } 240 } 241 242 // proceed to the next filter 243 chain.doFilter(httpRequest, response); 244 } 245 246 private Subject getFoafAgentSubject() { 247 if (FOAF_AGENT_SUBJECT == null) { 248 FOAF_AGENT_SUBJECT = new Subject.Builder().principals(FOAF_AGENT_PRINCIPAL_COLLECTION).buildSubject(); 249 } 250 return FOAF_AGENT_SUBJECT; 251 } 252 253 private Transaction transaction(final HttpServletRequest request) { 254 final String txId = request.getHeader(ATOMIC_ID_HEADER); 255 if (txId == null) { 256 return null; 257 } 258 final var txProvider = new TransactionProvider(transactionManager, request, getBaseUri(request)); 259 return txProvider.provide(); 260 } 261 262 private String getContainerUrl(final HttpServletRequest servletRequest) { 263 final String pathInfo = servletRequest.getPathInfo(); 264 final String baseUrl = servletRequest.getRequestURL().toString().replace(pathInfo, ""); 265 final String[] paths = pathInfo.split("/"); 266 final String[] parentPaths = java.util.Arrays.copyOfRange(paths, 0, paths.length - 1); 267 return baseUrl + String.join("/", parentPaths); 268 } 269 270 private FedoraResource getContainer(final HttpServletRequest servletRequest) { 271 final FedoraResource resource = resource(servletRequest); 272 if (resource != null) { 273 return resource(servletRequest).getContainer(); 274 } 275 final String parentURI = getContainerUrl(servletRequest); 276 return resource(servletRequest, getIdFromRequest(servletRequest, parentURI)); 277 } 278 279 private FedoraResource resource(final HttpServletRequest servletRequest) { 280 return resource(servletRequest, getIdFromRequest(servletRequest)); 281 } 282 283 private FedoraResource resource(final HttpServletRequest servletRequest, final FedoraId resourceId) { 284 try { 285 return this.resourceFactory.getResource(transaction(servletRequest), resourceId); 286 } catch (final PathNotFoundException e) { 287 return null; 288 } 289 } 290 291 private FedoraId getIdFromRequest(final HttpServletRequest servletRequest) { 292 final String httpURI = servletRequest.getRequestURL().toString(); 293 return getIdFromRequest(servletRequest, httpURI); 294 } 295 296 private FedoraId getIdFromRequest(final HttpServletRequest request, final String httpURI) { 297 return FedoraId.create(identifierConverter(request).toInternalId(httpURI)); 298 } 299 300 private boolean isAuthorized(final Subject currentUser, final HttpServletRequest httpRequest) throws IOException { 301 final String requestURL = httpRequest.getRequestURL().toString(); 302 final boolean isAcl = requestURL.endsWith(FCR_ACL); 303 final boolean isTxEndpoint = requestURL.endsWith(FCR_TX) || requestURL.endsWith(FCR_TX + "/"); 304 final URI requestURI = URI.create(requestURL); 305 log.debug("Request URI is {}", requestURI); 306 final FedoraResource resource = resource(httpRequest); 307 final FedoraResource container = getContainer(httpRequest); 308 309 // WebAC permissions 310 final WebACPermission toRead = new WebACPermission(WEBAC_MODE_READ, requestURI); 311 final WebACPermission toWrite = new WebACPermission(WEBAC_MODE_WRITE, requestURI); 312 final WebACPermission toAppend = new WebACPermission(WEBAC_MODE_APPEND, requestURI); 313 final WebACPermission toControl = new WebACPermission(WEBAC_MODE_CONTROL, requestURI); 314 315 switch (httpRequest.getMethod()) { 316 case "OPTIONS": 317 case "HEAD": 318 case "GET": 319 if (isAcl) { 320 if (currentUser.isPermitted(toControl)) { 321 log.debug("GET allowed by {} permission", toControl); 322 return true; 323 } else { 324 log.debug("GET prohibited without {} permission", toControl); 325 return false; 326 } 327 } else { 328 return currentUser.isPermitted(toRead); 329 } 330 case "PUT": 331 if (isAcl) { 332 if (currentUser.isPermitted(toControl)) { 333 log.debug("PUT allowed by {} permission", toControl); 334 return true; 335 } else { 336 log.debug("PUT prohibited without {} permission", toControl); 337 return false; 338 } 339 } else if (currentUser.isPermitted(toWrite)) { 340 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 341 log.debug("PUT denied, not authorized to write to membershipRelation"); 342 return false; 343 } 344 log.debug("PUT allowed by {} permission", toWrite); 345 return true; 346 } else { 347 if (resource != null) { 348 // can't PUT to an existing resource without acl:Write permission 349 log.debug("PUT prohibited to existing resource without {} permission", toWrite); 350 return false; 351 } else { 352 // find nearest parent resource and verify that user has acl:Append on it 353 // this works because when the authorizations are inherited, it is the target request URI that is 354 // added as the resource, not the accessTo or other URI in the original authorization 355 log.debug("Resource doesn't exist; checking parent resources for acl:Append permission"); 356 if (currentUser.isPermitted(toAppend)) { 357 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 358 log.debug("PUT denied, not authorized to write to membershipRelation"); 359 return false; 360 } 361 log.debug("PUT allowed for new resource by inherited {} permission", toAppend); 362 return true; 363 } else { 364 log.debug("PUT prohibited for new resource without inherited {} permission", toAppend); 365 return false; 366 } 367 } 368 } 369 case "POST": 370 if (isTxEndpoint && currentUser.isAuthenticated()) { 371 final String currentUsername = ((Principal) currentUser.getPrincipal()).getName(); 372 log.debug("POST allowed to transaction endpoint for authenticated user {}", currentUsername); 373 return true; 374 } 375 if (currentUser.isPermitted(toWrite)) { 376 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 377 log.debug("POST denied, not authorized to write to membershipRelation"); 378 return false; 379 } 380 log.debug("POST allowed by {} permission", toWrite); 381 return true; 382 } 383 if (resource != null) { 384 if (isBinaryOrDescription(resource)) { 385 // LDP-NR 386 // user without the acl:Write permission cannot POST to binaries 387 log.debug("POST prohibited to binary resource without {} permission", toWrite); 388 return false; 389 } else { 390 // LDP-RS 391 // user with the acl:Append permission may POST to containers 392 if (currentUser.isPermitted(toAppend)) { 393 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 394 log.debug("POST denied, not authorized to write to membershipRelation"); 395 return false; 396 } 397 log.debug("POST allowed to container by {} permission", toAppend); 398 return true; 399 } else { 400 log.debug("POST prohibited to container without {} permission", toAppend); 401 return false; 402 } 403 } 404 } else { 405 // prohibit POST to non-existent resources without the acl:Write permission 406 log.debug("POST prohibited to non-existent resource without {} permission", toWrite); 407 return false; 408 } 409 case "DELETE": 410 if (isAcl) { 411 if (currentUser.isPermitted(toControl)) { 412 log.debug("DELETE allowed by {} permission", toControl); 413 return true; 414 } else { 415 log.debug("DELETE prohibited without {} permission", toControl); 416 return false; 417 } 418 } else { 419 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 420 log.debug("DELETE denied, not authorized to write to membershipRelation"); 421 return false; 422 } 423 return currentUser.isPermitted(toWrite); 424 } 425 case "PATCH": 426 427 if (isAcl) { 428 if (currentUser.isPermitted(toControl)) { 429 log.debug("PATCH allowed by {} permission", toControl); 430 return true; 431 } else { 432 log.debug("PATCH prohibited without {} permission", toControl); 433 return false; 434 } 435 } else if (currentUser.isPermitted(toWrite)) { 436 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 437 log.debug("PATCH denied, not authorized to write to membershipRelation"); 438 return false; 439 } 440 return true; 441 } else { 442 if (currentUser.isPermitted(toAppend)) { 443 if (!isAuthorizedForMembershipResource(httpRequest, currentUser, resource, container)) { 444 log.debug("PATCH denied, not authorized to write to membershipRelation"); 445 return false; 446 } 447 return isPatchContentPermitted(httpRequest); 448 } 449 } 450 return false; 451 default: 452 return false; 453 } 454 } 455 456 private boolean isPatchContentPermitted(final HttpServletRequest httpRequest) throws IOException { 457 if (!isSparqlUpdate(httpRequest)) { 458 log.debug("Cannot verify authorization on NON-SPARQL Patch request."); 459 return false; 460 } 461 if (httpRequest.getInputStream() != null) { 462 boolean noDeletes = false; 463 try { 464 noDeletes = !hasDeleteClause(IOUtils.toString(httpRequest.getInputStream(), UTF_8)); 465 } catch (final QueryParseException ex) { 466 log.error("Cannot verify authorization! Exception while inspecting SPARQL query!", ex); 467 } 468 return noDeletes; 469 } else { 470 log.debug("Authorizing SPARQL request with no content."); 471 return true; 472 } 473 } 474 475 private boolean hasDeleteClause(final String sparqlString) { 476 final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString); 477 return sparqlUpdate.getOperations().stream() 478 .filter(update -> update instanceof UpdateDataDelete) 479 .map(update -> (UpdateDataDelete) update) 480 .anyMatch(update -> update.getQuads().size() > 0) || 481 sparqlUpdate.getOperations().stream().filter(update -> (update instanceof UpdateModify)) 482 .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString())) 483 .map(update -> (UpdateModify)update) 484 .filter(UpdateModify::hasDeleteClause) 485 .anyMatch(update -> update.getDeleteQuads().size() > 0); 486 } 487 488 private boolean isSparqlUpdate(final HttpServletRequest request) { 489 try { 490 return request.getMethod().equals("PATCH") && 491 sparqlUpdate.isCompatible(MediaType.valueOf(request 492 .getContentType())); 493 } catch (final IllegalArgumentException e) { 494 return false; 495 } 496 } 497 498 /** 499 * Does the request's content-type match one of the RDF types. 500 * 501 * @param request the http servlet request 502 * @return whether the content-type matches. 503 */ 504 private boolean isRdfRequest(final HttpServletRequest request) { 505 return request.getContentType() != null && rdfContentTypes.contains(request.getContentType()); 506 } 507 508 /** 509 * Is the request to create an indirect or direct container. 510 * 511 * @param request The current request 512 * @return whether we are acting on/creating an indirect/direct container. 513 */ 514 private boolean isPayloadIndirectOrDirect(final HttpServletRequest request) { 515 return Collections.list(request.getHeaders("Link")).stream().map(Link::valueOf).map(Link::getUri) 516 .anyMatch(l -> directOrIndirect.contains(l)); 517 } 518 519 /** 520 * Is the current resource a direct or indirect container 521 * 522 * @param resource the resource to check 523 * @return whether it is a direct or indirect container. 524 */ 525 private boolean isResourceIndirectOrDirect(final FedoraResource resource) { 526 return resource != null && resource.getTypes().stream().anyMatch(l -> directOrIndirect.contains(l)); 527 } 528 529 /** 530 * Check if we are authorized to access the target of membershipRelation if required. Really this is a test for 531 * failure. The default is true because we might not be looking at an indirect or direct container. 532 * 533 * @param request The current request 534 * @param currentUser The current principal 535 * @param resource The resource 536 * @param container The container 537 * @return Whether we are creating an indirect/direct container and can write the membershipRelation 538 * @throws IOException when getting request's inputstream 539 */ 540 private boolean isAuthorizedForMembershipResource(final HttpServletRequest request, final Subject currentUser, 541 final FedoraResource resource, final FedoraResource container) 542 throws IOException { 543 if (resource != null && request.getMethod().equalsIgnoreCase("POST")) { 544 // Check resource if it exists and we are POSTing to it. 545 if (isResourceIndirectOrDirect(resource)) { 546 final URI membershipResource = getHasMemberFromResource(request); 547 addURIToAuthorize(request, membershipResource); 548 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 549 return false; 550 } 551 } 552 } else if (request.getMethod().equalsIgnoreCase("PUT")) { 553 // PUT to a URI check that the immediate container is not direct or indirect. 554 if (isResourceIndirectOrDirect(container)) { 555 final URI membershipResource = getHasMemberFromResource(request, container); 556 addURIToAuthorize(request, membershipResource); 557 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 558 return false; 559 } 560 } 561 } else if (isSparqlUpdate(request) && isResourceIndirectOrDirect(resource)) { 562 // PATCH to a direct/indirect might change the ldp:membershipResource 563 final URI membershipResource = getHasMemberFromPatch(request); 564 if (membershipResource != null) { 565 log.debug("Found membership resource: {}", membershipResource); 566 // add the membership URI to the list URIs to retrieve ACLs for 567 addURIToAuthorize(request, membershipResource); 568 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 569 return false; 570 } 571 } 572 } else if (request.getMethod().equalsIgnoreCase("DELETE")) { 573 if (isResourceIndirectOrDirect(resource)) { 574 // If we delete a direct/indirect container we have to have access to the ldp:membershipResource 575 final URI membershipResource = getHasMemberFromResource(request); 576 addURIToAuthorize(request, membershipResource); 577 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 578 return false; 579 } 580 } else if (isResourceIndirectOrDirect(container)) { 581 // or if we delete a child of a direct/indirect container we have to have access to the 582 // ldp:membershipResource 583 final URI membershipResource = getHasMemberFromResource(request, container); 584 addURIToAuthorize(request, membershipResource); 585 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 586 return false; 587 } 588 } 589 } 590 591 if (isPayloadIndirectOrDirect(request)) { 592 // Check if we are creating a direct/indirect container. 593 final URI membershipResource = getHasMemberFromRequest(request); 594 if (membershipResource != null) { 595 log.debug("Found membership resource: {}", membershipResource); 596 // add the membership URI to the list URIs to retrieve ACLs for 597 addURIToAuthorize(request, membershipResource); 598 if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) { 599 return false; 600 } 601 } 602 } 603 // Not indirect/directs or we are authorized. 604 return true; 605 } 606 607 /** 608 * Get the memberRelation object from the contents. 609 * 610 * @param request The request. 611 * @return The URI of the memberRelation object 612 * @throws IOException when getting request's inputstream 613 */ 614 private URI getHasMemberFromRequest(final HttpServletRequest request) throws IOException { 615 final String baseUri = request.getRequestURL().toString(); 616 final RDFReader reader; 617 final String contentType = request.getContentType(); 618 final Lang format = contentTypeToLang(contentType); 619 final Model inputModel; 620 try { 621 inputModel = createDefaultModel(); 622 reader = inputModel.getReader(format.getName().toUpperCase()); 623 reader.read(inputModel, request.getInputStream(), baseUri); 624 final Statement st = inputModel.getProperty(null, MEMBERSHIP_RESOURCE); 625 return (st != null ? URI.create(st.getObject().toString()) : null); 626 } catch (final RiotException e) { 627 throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e); 628 } catch (final RuntimeIOException e) { 629 if (e.getCause() instanceof JsonParseException) { 630 final var cause = e.getCause(); 631 throw new MalformedRdfException(cause.getMessage(), cause); 632 } 633 throw new RepositoryRuntimeException(e.getMessage(), e); 634 } 635 } 636 637 /** 638 * Get the membershipRelation from a PATCH request 639 * 640 * @param request the http request 641 * @return URI of the first ldp:membershipRelation object. 642 * @throws IOException converting the request body to a string. 643 */ 644 private URI getHasMemberFromPatch(final HttpServletRequest request) throws IOException { 645 final String sparqlString = IOUtils.toString(request.getInputStream(), UTF_8); 646 final String baseURI = request.getRequestURL().toString().replace(request.getContextPath(), "").replaceAll( 647 request.getPathInfo(), "").replaceAll("rest$", ""); 648 final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString); 649 // The INSERT|DELETE DATA quads 650 final Stream<Quad> insertDeleteData = sparqlUpdate.getOperations().stream() 651 .filter(update -> update instanceof UpdateData) 652 .map(update -> (UpdateData) update) 653 .flatMap(update -> update.getQuads().stream()); 654 // Get the UpdateModify instance to re-use below. 655 final List<UpdateModify> updateModifyStream = sparqlUpdate.getOperations().stream() 656 .filter(update -> (update instanceof UpdateModify)) 657 .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString())) 658 .map(update -> (UpdateModify) update) 659 .collect(toList()); 660 // The INSERT {} WHERE {} quads 661 final Stream<Quad> insertQuadData = updateModifyStream.stream() 662 .flatMap(update -> update.getInsertQuads().stream()); 663 // The DELETE {} WHERE {} quads 664 final Stream<Quad> deleteQuadData = updateModifyStream.stream() 665 .flatMap(update -> update.getDeleteQuads().stream()); 666 // The ldp:membershipResource triples. 667 return Stream.concat(Stream.concat(insertDeleteData, insertQuadData), deleteQuadData) 668 .filter(update -> update.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && update.getObject() 669 .isURI()) 670 .map(update -> update.getObject().getURI()) 671 .map(update -> update.replace("file:///", baseURI)) 672 .findFirst().map(URI::create).orElse(null); 673 } 674 675 /** 676 * Get ldp:membershipResource from an existing resource 677 * 678 * @param request the request 679 * @return URI of the ldp:membershipResource triple or null if not found. 680 */ 681 private URI getHasMemberFromResource(final HttpServletRequest request) { 682 final FedoraResource resource = resource(request); 683 return getHasMemberFromResource(request, resource); 684 } 685 686 /** 687 * Get ldp:membershipResource from an existing resource 688 * 689 * @param request the request 690 * @param resource the FedoraResource 691 * @return URI of the ldp:membershipResource triple or null if not found. 692 */ 693 private URI getHasMemberFromResource(final HttpServletRequest request, final FedoraResource resource) { 694 return resource.getTriples() 695 .filter(triple -> triple.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && triple.getObject() 696 .isURI()) 697 .map(Triple::getObject).map(Node::getURI) 698 .findFirst().map(URI::create).orElse(null); 699 } 700 701 /** 702 * Determine if the resource is a binary or a binary description. 703 * @param resource the fedora resource to check 704 * @return true if a binary or binary description. 705 */ 706 private static boolean isBinaryOrDescription(final FedoraResource resource) { 707 return resource.getTypes().stream().map(URI::toString) 708 .anyMatch(t -> t.equals(NON_RDF_SOURCE.toString()) || t.equals(FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI)); 709 } 710 711}