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