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