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