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