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}