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