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