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}