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