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