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