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 */
018package org.fcrepo.http.api;
019
020import static java.nio.charset.StandardCharsets.UTF_8;
021import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
022import static javax.ws.rs.core.Response.created;
023import static javax.ws.rs.core.Response.noContent;
024import static javax.ws.rs.core.Response.ok;
025import static javax.ws.rs.core.Response.Status.CONFLICT;
026import static org.apache.commons.lang3.StringUtils.isBlank;
027import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
028import static org.apache.jena.rdf.model.ResourceFactory.createResource;
029import static org.apache.jena.riot.Lang.TTL;
030import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate;
031import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD;
032import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2_WITH_CHARSET;
033import static org.fcrepo.http.commons.domain.RDFMediaType.N3_WITH_CHARSET;
034import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES;
035import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML;
036import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET;
037import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET;
038import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_WITH_CHARSET;
039import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X;
040import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_REPOSITORY_ROOT;
041import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL;
042import static org.slf4j.LoggerFactory.getLogger;
043
044import java.io.File;
045import java.io.IOException;
046import java.io.InputStream;
047import java.net.URI;
048import javax.jcr.ItemNotFoundException;
049import javax.servlet.http.HttpServletResponse;
050import javax.ws.rs.BadRequestException;
051import javax.ws.rs.ClientErrorException;
052import javax.ws.rs.Consumes;
053import javax.ws.rs.DELETE;
054import javax.ws.rs.GET;
055import javax.ws.rs.HeaderParam;
056import javax.ws.rs.PUT;
057import javax.ws.rs.Path;
058import javax.ws.rs.PathParam;
059import javax.ws.rs.Produces;
060import javax.ws.rs.core.Context;
061import javax.ws.rs.core.MediaType;
062import javax.ws.rs.core.Request;
063import javax.ws.rs.core.Response;
064import javax.ws.rs.core.UriInfo;
065
066import org.apache.commons.io.IOUtils;
067import org.apache.jena.rdf.model.Model;
068import org.apache.jena.riot.RDFDataMgr;
069import org.apache.jena.shared.JenaException;
070import org.fcrepo.http.api.PathLockManager.AcquiredLock;
071import org.fcrepo.http.commons.domain.PATCH;
072import org.fcrepo.http.commons.domain.RDFMediaType;
073import org.fcrepo.http.commons.responses.RdfNamespacedStream;
074import org.fcrepo.kernel.api.RdfStream;
075import org.fcrepo.kernel.api.exception.AccessDeniedException;
076import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
077import org.fcrepo.kernel.api.models.FedoraResource;
078import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
079import org.slf4j.Logger;
080import org.springframework.context.annotation.Scope;
081
082/**
083 * @author lsitu
084 * @author peichman
085 * @since 4/20/18
086 */
087@Scope("request")
088@Path("/{path: (.+/)?}fcr:acl")
089public class FedoraAcl extends ContentExposingResource {
090
091    private static final Logger LOGGER = getLogger(FedoraAcl.class);
092
093    public static final String ROOT_AUTHORIZATION_PROPERTY = "fcrepo.auth.webac.authorization";
094
095    private static final String ROOT_AUTHORIZATION_LOCATION = "/root-authorization.ttl";
096
097    @Context protected Request request;
098    @Context protected HttpServletResponse servletResponse;
099    @Context protected UriInfo uriInfo;
100
101    @PathParam("path") protected String externalPath;
102
103    /**
104     * Default JAX-RS entry point
105     */
106    public FedoraAcl() {
107        super();
108    }
109
110    /**
111     * PUT to create FedoraWebacACL resource.
112     * @param requestContentType The content type of the resource body
113     * @param requestBodyStream  The request body as stream
114     * @return the response for a request to create a Fedora WebAc acl
115     */
116    @PUT
117    public Response createFedoraWebacAcl(@HeaderParam(CONTENT_TYPE) final MediaType requestContentType,
118                                         final InputStream requestBodyStream) {
119
120        if (resource().isAcl() || resource().isMemento()) {
121            throw new BadRequestException("ACL resource creation is not allowed for resource " + resource().getPath());
122        }
123
124        final boolean created;
125        final FedoraResource aclResource;
126
127        final String path = toPath(translator(), externalPath);
128        final AcquiredLock lock = lockManager.lockForWrite(path, session.getFedoraSession(), nodeService);
129        try {
130            LOGGER.info("PUT acl resource '{}'", externalPath);
131
132            aclResource = resource().findOrCreateAcl();
133            created = aclResource.isNew();
134
135            final MediaType contentType =
136                getSimpleContentType(requestContentType == null ? RDFMediaType.TURTLE_TYPE : requestContentType);
137            if (isRdfContentType(contentType.toString())) {
138
139                try (final RdfStream resourceTriples =
140                         created ? new DefaultRdfStream(asNode(aclResource)) : getResourceTriples(aclResource)) {
141                    replaceResourceWithStream(aclResource, requestBodyStream, contentType, resourceTriples);
142
143                }
144            } else {
145                throw new BadRequestException("Content-Type (" + requestContentType + ") is invalid. Try text/turtle " +
146                                              "or other RDF compatible type.");
147            }
148            session.commit();
149        } finally {
150            lock.release();
151        }
152
153        addCacheControlHeaders(servletResponse, aclResource, session);
154        final URI location = getUri(aclResource);
155        if (created) {
156            return created(location).build();
157        } else {
158            return noContent().location(location).build();
159        }
160    }
161
162    /**
163     * PATCH to update an FedoraWebacACL resource using SPARQL-UPDATE
164     *
165     * @param requestBodyStream the request body stream
166     * @return 204
167     * @throws IOException if IO exception occurred
168     */
169    @PATCH
170    @Consumes({ contentTypeSPARQLUpdate })
171    public Response updateSparql(final InputStream requestBodyStream)
172        throws IOException, ItemNotFoundException {
173        hasRestrictedPath(externalPath);
174
175        if (null == requestBodyStream) {
176            throw new BadRequestException("SPARQL-UPDATE requests must have content!");
177        }
178
179        final FedoraResource aclResource = resource().getAcl();
180
181        if (aclResource == null) {
182            if (resource().hasType(FEDORA_REPOSITORY_ROOT)) {
183                throw new ClientErrorException("The default root ACL is system generated and cannot be modified. " +
184                        "To override the default root ACL you must PUT a user-defined ACL to this endpoint.",
185                        CONFLICT);
186            }
187
188            throw new ItemNotFoundException();
189        }
190
191        final AcquiredLock lock = lockManager.lockForWrite(aclResource.getPath(), session.getFedoraSession(),
192                                                           nodeService);
193
194        try {
195            final String requestBody = IOUtils.toString(requestBodyStream, UTF_8);
196            if (isBlank(requestBody)) {
197                throw new BadRequestException("SPARQL-UPDATE requests must have content!");
198            }
199
200            evaluateRequestPreconditions(request, servletResponse, aclResource, session);
201
202            try (final RdfStream resourceTriples =
203                     aclResource.isNew() ? new DefaultRdfStream(asNode(aclResource)) :
204                     getResourceTriples(aclResource)) {
205                LOGGER.info("PATCH for '{}'", externalPath);
206                patchResourcewithSparql(aclResource, requestBody, resourceTriples);
207            }
208            session.commit();
209
210            addCacheControlHeaders(servletResponse, aclResource, session);
211
212            return noContent().build();
213        } catch (final IllegalArgumentException iae) {
214            throw new BadRequestException(iae.getMessage());
215        } catch (final AccessDeniedException e) {
216            throw e;
217        } catch (final RuntimeException ex) {
218            final Throwable cause = ex.getCause();
219            if (cause instanceof PathNotFoundRuntimeException) {
220                // the sparql update referred to a repository resource that doesn't exist
221                throw new BadRequestException(cause.getMessage());
222            }
223            throw ex;
224        } finally {
225            lock.release();
226        }
227    }
228
229    @Override
230    protected String externalPath() {
231        return externalPath;
232    }
233
234    /**
235     * GET to retrieve the ACL resource.
236     *
237     * @param rangeValue the range value
238     * @return a binary or the triples for the specified node
239     * @throws IOException if IO exception occurred
240     */
241    @GET
242    @Produces({ TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8",
243                N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET,
244                TURTLE_X, TEXT_HTML_WITH_CHARSET })
245    public Response getResource(@HeaderParam("Range") final String rangeValue)
246            throws IOException, ItemNotFoundException {
247
248        LOGGER.info("GET resource '{}'", externalPath);
249
250        final FedoraResource aclResource = resource().getAcl();
251
252        if (aclResource == null) {
253            if (resource().hasType(FEDORA_REPOSITORY_ROOT)) {
254                final String resourceUri = getUri(resource()).toString();
255                final String aclUri = resourceUri + (resourceUri.endsWith("/") ? "" : "/") + FCR_ACL;
256
257                final RdfStream defaultRdfStream = DefaultRdfStream.fromModel(createResource(aclUri).asNode(),
258                    getDefaultAcl(aclUri));
259                final RdfNamespacedStream output = new RdfNamespacedStream(defaultRdfStream,
260                    namespaceRegistry.getNamespaces());
261                return ok(output).build();
262            }
263
264            throw new ItemNotFoundException();
265        }
266
267        checkCacheControlHeaders(request, servletResponse, aclResource, session);
268
269        LOGGER.info("GET resource '{}'", externalPath);
270        final AcquiredLock readLock = lockManager.lockForRead(aclResource.getPath());
271        try (final RdfStream rdfStream = new DefaultRdfStream(asNode(aclResource))) {
272
273            addResourceHttpHeaders(aclResource);
274            return getContent(rangeValue, getChildrenLimit(), rdfStream, aclResource);
275
276        } finally {
277            readLock.release();
278        }
279    }
280
281    /**
282     * Deletes an object.
283     *
284     * @return response
285     */
286    @DELETE
287    public Response deleteObject() throws ItemNotFoundException {
288
289        hasRestrictedPath(externalPath);
290        LOGGER.info("Delete resource '{}'", externalPath);
291
292        final AcquiredLock lock = lockManager.lockForDelete(resource().getPath());
293
294        try {
295            final FedoraResource aclResource = resource().getAcl();
296            if (aclResource != null) {
297                aclResource.delete();
298            }
299            session.commit();
300
301            if (aclResource == null) {
302                if (resource().hasType(FEDORA_REPOSITORY_ROOT)) {
303                    throw new ClientErrorException("The default root ACL is system generated and cannot be deleted. " +
304                            "To override the default root ACL you must PUT a user-defined ACL to this endpoint.",
305                            CONFLICT);
306                }
307
308                throw new ItemNotFoundException();
309            }
310
311            return noContent().build();
312
313        } finally {
314            lock.release();
315        }
316    }
317
318    /**
319     * Retrieve the default root ACL from a user specified location if it exists,
320     * otherwise the one provided by Fedora will be used.
321     * @param baseUri the URI of the default ACL
322     * @return Model the rdf model of the default root ACL
323     */
324    public static Model getDefaultAcl(final String baseUri) {
325        final String rootAcl = System.getProperty(ROOT_AUTHORIZATION_PROPERTY);
326        final Model model = createDefaultModel();
327
328        if (rootAcl != null && new File(rootAcl).isFile()) {
329            try {
330                LOGGER.debug("Getting root authorization from file: {}", rootAcl);
331
332                RDFDataMgr.read(model, rootAcl, baseUri, null);
333
334                return model;
335            } catch (final JenaException ex) {
336                throw new RuntimeException("Error parsing the default root ACL " + rootAcl + ".", ex);
337            }
338        }
339
340        try (final InputStream is = FedoraAcl.class.getResourceAsStream(ROOT_AUTHORIZATION_LOCATION)) {
341            LOGGER.debug("Getting root ACL from classpath: {}", ROOT_AUTHORIZATION_LOCATION);
342
343            return model.read(is, baseUri, TTL.getName());
344        } catch (final IOException ex) {
345            throw new RuntimeException("Error reading the default root Acl " + ROOT_AUTHORIZATION_LOCATION + ".", ex);
346        } catch (final JenaException ex) {
347            throw new RuntimeException("Error parsing the default root ACL " + ROOT_AUTHORIZATION_LOCATION + ".", ex);
348        }
349    }
350}