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