001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.fcrepo.auth.webac;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022import static java.util.EnumSet.of;
023import static java.util.stream.Collectors.toList;
024import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
025import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
026import static org.apache.jena.riot.RDFLanguages.contentTypeToLang;
027import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate;
028import static org.apache.jena.riot.WebContent.contentTypeJSONLD;
029import static org.apache.jena.riot.WebContent.contentTypeTurtle;
030import static org.apache.jena.riot.WebContent.contentTypeRDFXML;
031import static org.apache.jena.riot.WebContent.contentTypeN3;
032import static org.apache.jena.riot.WebContent.contentTypeNTriples;
033import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_ADMIN_ROLE;
034import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_USER_ROLE;
035import static org.fcrepo.auth.webac.URIConstants.FOAF_AGENT_VALUE;
036import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_APPEND;
037import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_CONTROL;
038import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_READ;
039import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_WRITE;
040import static org.fcrepo.auth.webac.WebACAuthorizingRealm.URIS_TO_AUTHORIZE;
041import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL;
042import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY;
043import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
044import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
045import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE;
046import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
047import static org.slf4j.LoggerFactory.getLogger;
048
049import java.io.IOException;
050import java.net.URI;
051import java.security.Principal;
052import java.util.Collections;
053import java.util.HashSet;
054import java.util.List;
055import java.util.Set;
056import java.util.stream.Stream;
057
058import javax.inject.Inject;
059import javax.servlet.Filter;
060import javax.servlet.FilterChain;
061import javax.servlet.FilterConfig;
062import javax.servlet.ServletException;
063import javax.servlet.ServletRequest;
064import javax.servlet.ServletResponse;
065import javax.servlet.http.HttpServletRequest;
066import javax.servlet.http.HttpServletResponse;
067import javax.ws.rs.BadRequestException;
068import javax.ws.rs.core.Link;
069import javax.ws.rs.core.MediaType;
070import javax.ws.rs.core.UriBuilder;
071
072import org.apache.commons.io.IOUtils;
073import org.apache.jena.atlas.RuntimeIOException;
074import org.apache.jena.graph.Node;
075import org.apache.jena.graph.Triple;
076import org.apache.jena.query.QueryParseException;
077import org.apache.jena.rdf.model.Model;
078import org.apache.jena.rdf.model.ModelFactory;
079import org.apache.jena.rdf.model.RDFReader;
080import org.apache.jena.rdf.model.Resource;
081import org.apache.jena.rdf.model.Statement;
082import org.apache.jena.riot.Lang;
083import org.apache.jena.riot.RiotException;
084import org.apache.jena.sparql.core.Quad;
085import org.apache.jena.sparql.modify.request.UpdateData;
086import org.apache.jena.sparql.modify.request.UpdateDataDelete;
087import org.apache.jena.sparql.modify.request.UpdateModify;
088import org.apache.jena.update.UpdateFactory;
089import org.apache.jena.update.UpdateRequest;
090import org.apache.shiro.SecurityUtils;
091import org.apache.shiro.subject.PrincipalCollection;
092import org.apache.shiro.subject.SimplePrincipalCollection;
093import org.apache.shiro.subject.Subject;
094import org.fcrepo.http.api.FedoraLdp;
095import org.fcrepo.http.commons.api.rdf.HttpResourceConverter;
096import org.fcrepo.http.commons.session.HttpSession;
097import org.fcrepo.http.commons.session.SessionFactory;
098import org.fcrepo.kernel.api.FedoraSession;
099import org.fcrepo.kernel.api.exception.MalformedRdfException;
100import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
101import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
102import org.fcrepo.kernel.api.models.FedoraResource;
103import org.fcrepo.kernel.api.services.NodeService;
104import org.slf4j.Logger;
105
106import com.fasterxml.jackson.core.JsonParseException;
107
108/**
109 * @author peichman
110 */
111public class WebACFilter implements Filter {
112
113    private static final Logger log = getLogger(WebACFilter.class);
114
115    private static final MediaType sparqlUpdate = MediaType.valueOf(contentTypeSPARQLUpdate);
116
117    private FedoraSession session;
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 NodeService nodeService;
140
141    @Inject
142    private SessionFactory sessionFactory;
143
144    private static Set<URI> directOrIndirect = new HashSet<>();
145
146    private static Set<String> rdfContentTypes = new HashSet<>();
147
148    static {
149        directOrIndirect.add(URI.create(INDIRECT_CONTAINER.toString()));
150        directOrIndirect.add(URI.create(DIRECT_CONTAINER.toString()));
151
152        rdfContentTypes.add(contentTypeTurtle);
153        rdfContentTypes.add(contentTypeJSONLD);
154        rdfContentTypes.add(contentTypeN3);
155        rdfContentTypes.add(contentTypeRDFXML);
156        rdfContentTypes.add(contentTypeNTriples);
157    }
158    @Override
159    public void init(final FilterConfig filterConfig) {
160        // this method intentionally left empty
161    }
162
163    /**
164     * Add URIs to collect permissions information for.
165     *
166     * @param httpRequest the request.
167     * @param uri the uri to check.
168     */
169    private void addURIToAuthorize(final HttpServletRequest httpRequest, final URI uri) {
170        @SuppressWarnings("unchecked")
171        Set<URI> targetURIs = (Set<URI>) httpRequest.getAttribute(URIS_TO_AUTHORIZE);
172        if (targetURIs == null) {
173            targetURIs = new HashSet<>();
174            httpRequest.setAttribute(URIS_TO_AUTHORIZE, targetURIs);
175        }
176        targetURIs.add(uri);
177    }
178
179    @Override
180    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
181            throws IOException, ServletException {
182        final Subject currentUser = SecurityUtils.getSubject();
183        HttpServletRequest httpRequest = (HttpServletRequest) request;
184        if (isSparqlUpdate(httpRequest) || isRdfRequest(httpRequest)) {
185            // If this is a sparql request or contains RDF.
186            httpRequest = new CachedHttpRequest(httpRequest);
187        }
188
189        // add the request URI to the list of URIs to retrieve the ACLs for
190        addURIToAuthorize(httpRequest, URI.create(httpRequest.getRequestURL().toString()));
191
192        if (currentUser.isAuthenticated()) {
193            log.debug("User is authenticated");
194            if (currentUser.hasRole(FEDORA_ADMIN_ROLE)) {
195                log.debug("User has fedoraAdmin role");
196            } else if (currentUser.hasRole(FEDORA_USER_ROLE)) {
197                log.debug("User has fedoraUser role");
198                // non-admins are subject to permission checks
199                if (!isAuthorized(currentUser, httpRequest)) {
200                    // if the user is not authorized, set response to forbidden
201                    ((HttpServletResponse) response).sendError(SC_FORBIDDEN);
202                    return;
203                }
204            } else {
205                log.debug("User has no recognized servlet container role");
206                // missing a container role, return forbidden
207                ((HttpServletResponse) response).sendError(SC_FORBIDDEN);
208                return;
209            }
210        } else {
211            log.debug("User is NOT authenticated");
212            // anonymous users are subject to permission checks
213            if (!isAuthorized(getFoafAgentSubject(), httpRequest)) {
214                // if anonymous user is not authorized, set response to forbidden
215                ((HttpServletResponse) response).sendError(SC_FORBIDDEN);
216                return;
217            }
218        }
219
220        // proceed to the next filter
221        chain.doFilter(httpRequest, response);
222    }
223
224    private Subject getFoafAgentSubject() {
225        if (FOAF_AGENT_SUBJECT == null) {
226            FOAF_AGENT_SUBJECT = new Subject.Builder().principals(FOAF_AGENT_PRINCIPAL_COLLECTION).buildSubject();
227        }
228        return FOAF_AGENT_SUBJECT;
229    }
230
231    @Override
232    public void destroy() {
233        // this method intentionally left empty
234    }
235
236    private FedoraSession session() {
237        if (session == null) {
238            session = sessionFactory.getInternalSession();
239        }
240        return session;
241    }
242
243    private String getBaseURL(final HttpServletRequest servletRequest) {
244        final String url = servletRequest.getRequestURL().toString();
245        // the base URL will be the request URL if there is no path info
246        String baseURL = url;
247
248        // strip out the path info, if it exists
249        final String pathInfo = servletRequest.getPathInfo();
250        if (pathInfo != null) {
251            final int loc = url.lastIndexOf(pathInfo);
252            baseURL = url.substring(0, loc);
253        }
254
255        log.debug("Base URL determined from servlet request is {}", baseURL);
256        return baseURL;
257    }
258
259    private String getContainerUrl(final HttpServletRequest servletRequest) {
260        final String pathInfo = servletRequest.getPathInfo();
261        final String baseUrl = servletRequest.getRequestURL().toString().replace(pathInfo, "");
262        final String[] paths = pathInfo.split("/");
263        final String[] parentPaths = java.util.Arrays.copyOfRange(paths, 0, paths.length - 1);
264        return baseUrl + String.join("/", parentPaths);
265    }
266
267    private boolean containerExists(final HttpServletRequest servletRequest) {
268        if (resourceExists(servletRequest)) {
269            return true;
270        }
271        final String parentURI = getContainerUrl(servletRequest);
272        return nodeService.exists(session(), getRepoPath(servletRequest, parentURI));
273    }
274
275    private FedoraResource getContainer(final HttpServletRequest servletRequest) {
276        if (resourceExists(servletRequest)) {
277            return resource(servletRequest).getContainer();
278        }
279        final String parentURI = getContainerUrl(servletRequest);
280        return nodeService.find(session(), getRepoPath(servletRequest, parentURI));
281    }
282
283    private FedoraResource resource(final HttpServletRequest servletRequest) {
284        return nodeService.find(session(), getRepoPath(servletRequest));
285    }
286
287    private boolean resourceExists(final HttpServletRequest servletRequest) {
288        return nodeService.exists(session(), getRepoPath(servletRequest));
289    }
290
291    private IdentifierConverter<Resource, FedoraResource> translator(final HttpServletRequest servletRequest) {
292        final HttpSession httpSession = new HttpSession(session());
293        final UriBuilder uriBuilder = UriBuilder.fromUri(getBaseURL(servletRequest)).path(FedoraLdp.class);
294        return new HttpResourceConverter(httpSession, uriBuilder);
295    }
296
297    private String getRepoPath(final HttpServletRequest servletRequest) {
298        final String httpURI = servletRequest.getRequestURL().toString();
299        return getRepoPath(servletRequest, httpURI);
300    }
301
302    private String getRepoPath(final HttpServletRequest servletRequest, final String httpURI) {
303        final Resource resource = ModelFactory.createDefaultModel().createResource(httpURI);
304        final String repoPath = translator(servletRequest).asString(resource);
305        log.debug("Converted request URI {} to repo path {}", httpURI, repoPath);
306        return repoPath;
307    }
308
309    private boolean isAuthorized(final Subject currentUser, final HttpServletRequest httpRequest) throws IOException {
310        final String requestURL = httpRequest.getRequestURL().toString();
311        final boolean isAcl = requestURL.endsWith(FCR_ACL);
312        final URI requestURI = URI.create(requestURL);
313        log.debug("Request URI is {}", requestURI);
314
315        // WebAC permissions
316        final WebACPermission toRead = new WebACPermission(WEBAC_MODE_READ, requestURI);
317        final WebACPermission toWrite = new WebACPermission(WEBAC_MODE_WRITE, requestURI);
318        final WebACPermission toAppend = new WebACPermission(WEBAC_MODE_APPEND, requestURI);
319        final WebACPermission toControl = new WebACPermission(WEBAC_MODE_CONTROL, requestURI);
320
321        switch (httpRequest.getMethod()) {
322        case "OPTIONS":
323        case "HEAD":
324        case "GET":
325            if (isAcl) {
326                if (currentUser.isPermitted(toControl)) {
327                    log.debug("GET allowed by {} permission", toControl);
328                    return true;
329                } else {
330                    log.debug("GET prohibited without {} permission", toControl);
331                    return false;
332                }
333            } else {
334                return currentUser.isPermitted(toRead);
335            }
336        case "PUT":
337            if (isAcl) {
338                if (currentUser.isPermitted(toControl)) {
339                    log.debug("PUT allowed by {} permission", toControl);
340                    return true;
341                } else {
342                    log.debug("PUT prohibited without {} permission", toControl);
343                    return false;
344                }
345            } else if (currentUser.isPermitted(toWrite)) {
346                if (!isAuthorizedForMembershipResource(httpRequest, currentUser)) {
347                    log.debug("PUT denied, not authorized to write to membershipRelation");
348                    return false;
349                }
350                log.debug("PUT allowed by {} permission", toWrite);
351                return true;
352            } else {
353                if (resourceExists(httpRequest)) {
354                    // can't PUT to an existing resource without acl:Write permission
355                    log.debug("PUT prohibited to existing resource without {} permission", toWrite);
356                    return false;
357                } else {
358                    // find nearest parent resource and verify that user has acl:Append on it
359                    // this works because when the authorizations are inherited, it is the target request URI that is
360                    // added as the resource, not the accessTo or other URI in the original authorization
361                    log.debug("Resource doesn't exist; checking parent resources for acl:Append permission");
362                    if (currentUser.isPermitted(toAppend)) {
363                        if (!isAuthorizedForMembershipResource(httpRequest, currentUser)) {
364                            log.debug("PUT denied, not authorized to write to membershipRelation");
365                            return false;
366                        }
367                        log.debug("PUT allowed for new resource by inherited {} permission", toAppend);
368                        return true;
369                    } else {
370                        log.debug("PUT prohibited for new resource without inherited {} permission", toAppend);
371                        return false;
372                    }
373                }
374            }
375        case "POST":
376            if (currentUser.isPermitted(toWrite)) {
377                if (!isAuthorizedForMembershipResource(httpRequest, currentUser)) {
378                    log.debug("POST denied, not authorized to write to membershipRelation");
379                    return false;
380                }
381                log.debug("POST allowed by {} permission", toWrite);
382                return true;
383            }
384            if (resourceExists(httpRequest)) {
385                if (resource(httpRequest).hasType(FEDORA_BINARY)) {
386                    // LDP-NR
387                    // user without the acl:Write permission cannot POST to binaries
388                    log.debug("POST prohibited to binary resource without {} permission", toWrite);
389                    return false;
390                } else {
391                    // LDP-RS
392                    // user with the acl:Append permission may POST to containers
393                    if (currentUser.isPermitted(toAppend)) {
394                        if (!isAuthorizedForMembershipResource(httpRequest, currentUser)) {
395                            log.debug("POST denied, not authorized to write to membershipRelation");
396                            return false;
397                        }
398                        log.debug("POST allowed to container by {} permission", toAppend);
399                        return true;
400                    } else {
401                        log.debug("POST prohibited to container without {} permission", toAppend);
402                        return false;
403                    }
404                }
405            } else {
406                // prohibit POST to non-existent resources without the acl:Write permission
407                log.debug("POST prohibited to non-existent resource without {} permission", toWrite);
408                return false;
409            }
410        case "DELETE":
411            if (isAcl) {
412                if (currentUser.isPermitted(toControl)) {
413                    log.debug("DELETE allowed by {} permission", toControl);
414                    return true;
415                } else {
416                    log.debug("DELETE prohibited without {} permission", toControl);
417                    return false;
418                }
419            } else {
420                if (!isAuthorizedForMembershipResource(httpRequest, currentUser)) {
421                    log.debug("DELETE denied, not authorized to write to membershipRelation");
422                    return false;
423                }
424                return currentUser.isPermitted(toWrite);
425            }
426        case "PATCH":
427
428            if (isAcl) {
429                if (currentUser.isPermitted(toControl)) {
430                    log.debug("PATCH allowed by {} permission", toControl);
431                    return true;
432                } else {
433                    log.debug("PATCH prohibited without {} permission", toControl);
434                    return false;
435                }
436            } else if (currentUser.isPermitted(toWrite)) {
437                if (!isAuthorizedForMembershipResource(httpRequest, currentUser)) {
438                    log.debug("PATCH denied, not authorized to write to membershipRelation");
439                    return false;
440                }
441                return true;
442            } else {
443                if (currentUser.isPermitted(toAppend)) {
444                    if (!isAuthorizedForMembershipResource(httpRequest, currentUser)) {
445                        log.debug("PATCH denied, not authorized to write to membershipRelation");
446                        return false;
447                    }
448                    return isPatchContentPermitted(httpRequest);
449                }
450            }
451            return false;
452        default:
453            return false;
454        }
455    }
456
457    private boolean isPatchContentPermitted(final HttpServletRequest httpRequest) throws IOException {
458        if (!isSparqlUpdate(httpRequest)) {
459            log.debug("Cannot verify authorization on NON-SPARQL Patch request.");
460            return false;
461        }
462        if (httpRequest.getInputStream() != null) {
463            boolean noDeletes = false;
464            try {
465                noDeletes = !hasDeleteClause(IOUtils.toString(httpRequest.getInputStream(), UTF_8));
466            } catch (final QueryParseException ex) {
467                log.error("Cannot verify authorization! Exception while inspecting SPARQL query!", ex);
468            }
469            return noDeletes;
470        } else {
471            log.debug("Authorizing SPARQL request with no content.");
472            return true;
473        }
474    }
475
476    private boolean hasDeleteClause(final String sparqlString) {
477        final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString);
478        return sparqlUpdate.getOperations().stream()
479                .filter(update -> update instanceof UpdateDataDelete)
480                .map(update -> (UpdateDataDelete) update)
481                .anyMatch(update -> update.getQuads().size() > 0) ||
482                sparqlUpdate.getOperations().stream().filter(update -> (update instanceof UpdateModify))
483                .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString()))
484                .map(update -> (UpdateModify)update)
485                .filter(UpdateModify::hasDeleteClause)
486                .anyMatch(update -> update.getDeleteQuads().size() > 0);
487    }
488
489    private boolean isSparqlUpdate(final HttpServletRequest request) {
490        try {
491            return request.getMethod().equals("PATCH") &&
492                    sparqlUpdate.isCompatible(MediaType.valueOf(request
493                            .getContentType()));
494        } catch (final IllegalArgumentException e) {
495            return false;
496        }
497    }
498
499    /**
500     * Does the request's content-type match one of the RDF types.
501     *
502     * @param request the http servlet request
503     * @return whether the content-type matches.
504     */
505    private boolean isRdfRequest(final HttpServletRequest request) {
506        return rdfContentTypes.contains(request.getContentType());
507    }
508
509    /**
510     * Is the request to create an indirect or direct container.
511     *
512     * @param request The current request
513     * @return whether we are acting on/creating an indirect/direct container.
514     */
515    private boolean isPayloadIndirectOrDirect(final HttpServletRequest request) {
516        return Collections.list(request.getHeaders("Link")).stream().map(Link::valueOf).map(Link::getUri)
517                .anyMatch(l -> directOrIndirect.contains(l));
518    }
519
520    /**
521     * Is the current resource a direct or indirect container
522     *
523     * @param request
524     * @return
525     */
526    private boolean isResourceIndirectOrDirect(final FedoraResource resource) {
527        return resource.getTypes().stream().anyMatch(l -> directOrIndirect.contains(l));
528    }
529
530    /**
531     * Check if we are authorized to access the target of membershipRelation if required. Really this is a test for
532     * failure. The default is true because we might not be looking at an indirect or direct container.
533     *
534     * @param request The current request
535     * @param currentUser The current principal
536     * @return Whether we are creating an indirect/direct container and can write the membershipRelation
537     * @throws IOException when getting request's inputstream
538     */
539    private boolean isAuthorizedForMembershipResource(final HttpServletRequest request, final Subject currentUser)
540            throws IOException {
541        if (resourceExists(request) && request.getMethod().equalsIgnoreCase("POST")) {
542            // Check resource if it exists and we are POSTing to it.
543            if (isResourceIndirectOrDirect(resource(request))) {
544                final URI membershipResource = getHasMemberFromResource(request);
545                addURIToAuthorize(request, membershipResource);
546                if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) {
547                    return false;
548                }
549            }
550        } else if (request.getMethod().equalsIgnoreCase("PUT")) {
551            // PUT to a URI check that the immediate container is not direct or indirect.
552            if (containerExists(request) && isResourceIndirectOrDirect(getContainer(request))) {
553                final URI membershipResource = getHasMemberFromResource(request, getContainer(request));
554                addURIToAuthorize(request, membershipResource);
555                if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) {
556                    return false;
557                }
558            }
559        } else if (isSparqlUpdate(request) && isResourceIndirectOrDirect(resource(request))) {
560            // PATCH to a direct/indirect might change the ldp:membershipResource
561            final URI membershipResource = getHasMemberFromPatch(request);
562            if (membershipResource != null) {
563                log.debug("Found membership resource: {}", membershipResource);
564                // add the membership URI to the list URIs to retrieve ACLs for
565                addURIToAuthorize(request, membershipResource);
566                if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) {
567                    return false;
568                }
569            }
570        } else if (request.getMethod().equalsIgnoreCase("DELETE")) {
571            if (isResourceIndirectOrDirect(resource(request))) {
572                // If we delete a direct/indirect container we have to have access to the ldp:membershipResource
573                final URI membershipResource = getHasMemberFromResource(request);
574                addURIToAuthorize(request, membershipResource);
575                if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) {
576                    return false;
577                }
578            } else if (isResourceIndirectOrDirect(getContainer(request))) {
579                // or if we delete a child of a direct/indirect container we have to have access to the
580                // ldp:membershipResource
581                final FedoraResource container = getContainer(request);
582                final URI membershipResource = getHasMemberFromResource(request, container);
583                addURIToAuthorize(request, membershipResource);
584                if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) {
585                    return false;
586                }
587            }
588        }
589
590        if (isPayloadIndirectOrDirect(request)) {
591            // Check if we are creating a direct/indirect container.
592            final URI membershipResource = getHasMemberFromRequest(request);
593            if (membershipResource != null) {
594                log.debug("Found membership resource: {}", membershipResource);
595                // add the membership URI to the list URIs to retrieve ACLs for
596                addURIToAuthorize(request, membershipResource);
597                if (!currentUser.isPermitted(new WebACPermission(WEBAC_MODE_WRITE, membershipResource))) {
598                    return false;
599                }
600            }
601        }
602        // Not indirect/directs or we are authorized.
603        return true;
604    }
605
606    /**
607     * Get the memberRelation object from the contents.
608     *
609     * @param baseUri The current request URL
610     * @param body The request body
611     * @param contentType The content type.
612     * @return The URI of the memberRelation object
613     * @throws IOException when getting request's inputstream
614     */
615    private URI getHasMemberFromRequest(final HttpServletRequest request) throws IOException {
616        final String baseUri = request.getRequestURL().toString();
617        final RDFReader reader;
618        final String contentType = request.getContentType();
619        final Lang format = contentTypeToLang(contentType);
620        final Model inputModel;
621        try {
622            inputModel = createDefaultModel();
623            reader = inputModel.getReader(format.getName().toUpperCase());
624            reader.read(inputModel, request.getInputStream(), baseUri);
625            final Statement st = inputModel.getProperty(null, MEMBERSHIP_RESOURCE);
626            return (st != null ? URI.create(st.getObject().toString()) : null);
627        } catch (final RiotException e) {
628            throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e);
629        } catch (final RuntimeIOException e) {
630            if (e.getCause() instanceof JsonParseException) {
631                throw new MalformedRdfException(e.getCause());
632            }
633            throw new RepositoryRuntimeException(e);
634        }
635    }
636
637    /**
638     * Get the membershipRelation from a PATCH request
639     *
640     * @param request the http request
641     * @return URI of the first ldp:membershipRelation object.
642     * @throws IOException converting the request body to a string.
643     */
644    private URI getHasMemberFromPatch(final HttpServletRequest request) throws IOException {
645        final String sparqlString = IOUtils.toString(request.getInputStream(), UTF_8);
646        final String baseURI = request.getRequestURL().toString().replace(request.getContextPath(), "").replaceAll(
647                request.getPathInfo(), "").replaceAll("rest$", "");
648        final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString);
649        // The INSERT|DELETE DATA quads
650        final Stream<Quad> insertDeleteData = sparqlUpdate.getOperations().stream()
651                .filter(update -> update instanceof UpdateData)
652                .map(update -> (UpdateData) update)
653                .flatMap(update -> update.getQuads().stream());
654        // Get the UpdateModify instance to re-use below.
655        final List<UpdateModify> updateModifyStream = sparqlUpdate.getOperations().stream()
656                .filter(update -> (update instanceof UpdateModify))
657                .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString()))
658                .map(update -> (UpdateModify) update)
659                .collect(toList());
660        // The INSERT {} WHERE {} quads
661        final Stream<Quad> insertQuadData = updateModifyStream.stream()
662                .flatMap(update -> update.getInsertQuads().stream());
663        // The DELETE {} WHERE {} quads
664        final Stream<Quad> deleteQuadData = updateModifyStream.stream()
665                .flatMap(update -> update.getDeleteQuads().stream());
666        // The ldp:membershipResource triples.
667        return Stream.concat(Stream.concat(insertDeleteData, insertQuadData), deleteQuadData)
668                .filter(update -> update.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && update.getObject()
669                        .isURI())
670                .map(update -> update.getObject().getURI())
671                .map(update -> update.replace("file:///", baseURI))
672                .findFirst().map(URI::create).orElse(null);
673    }
674
675    /**
676     * Get ldp:membershipResource from an existing resource
677     *
678     * @param request the request
679     * @return URI of the ldp:membershipResource triple or null if not found.
680     */
681    private URI getHasMemberFromResource(final HttpServletRequest request) {
682        final FedoraResource resource = resource(request);
683        return getHasMemberFromResource(request, resource);
684    }
685
686    /**
687     * Get ldp:membershipResource from an existing resource
688     *
689     * @param request the request
690     * @param resource the FedoraResource
691     * @return URI of the ldp:membershipResource triple or null if not found.
692     */
693    private URI getHasMemberFromResource(final HttpServletRequest request, final FedoraResource resource) {
694        return resource.getTriples(translator(request), of(PROPERTIES))
695                .filter(triple -> triple.getPredicate().equals(MEMBERSHIP_RESOURCE.asNode()) && triple.getObject()
696                        .isURI())
697                .map(Triple::getObject).map(Node::getURI)
698                .findFirst().map(URI::create).orElse(null);
699    }
700}