From 8310f770a8266cf33e97adea87188f050e975947 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Mon, 4 May 2020 14:10:59 -0500 Subject: [PATCH] Implement adding ACLs (#1672) * Implement adding ACLs --- .../http/api/ContentExposingResource.java | 8 ++ .../java/org/fcrepo/http/api/FedoraAcl.java | 95 +++++++++---------- .../java/org/fcrepo/http/api/FedoraLdp.java | 8 -- .../integration/http/api/FedoraAclIT.java | 20 +--- .../integration/http/api/FedoraLdpIT.java | 7 +- .../integration/http/api/StateTokensIT.java | 3 - .../api/rdf/HttpIdentifierConverter.java | 12 ++- .../org/fcrepo/kernel/api/RdfLexicon.java | 4 + .../kernel/api/identifiers/FedoraId.java | 68 +++++++++---- .../kernel/api/services/WebacAclService.java | 26 ++++- .../kernel/api/identifiers/FedoraIdTest.java | 12 ++- .../kernel/impl/ContainmentIndexImpl.java | 3 +- .../models/NonRdfSourceDescriptionImpl.java | 4 +- .../impl/models/ResourceFactoryImpl.java | 22 +++-- .../kernel/impl/models/WebacAclImpl.java | 65 +++++++++++++ .../kernel/impl/services/AbstractService.java | 5 +- .../ReplacePropertiesServiceImpl.java | 2 +- .../impl/services/WebacAclServiceImpl.java | 62 ++++++++++-- 18 files changed, 292 insertions(+), 134 deletions(-) create mode 100644 fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/WebacAclImpl.java diff --git a/fcrepo-http-api/src/main/java/org/fcrepo/http/api/ContentExposingResource.java b/fcrepo-http-api/src/main/java/org/fcrepo/http/api/ContentExposingResource.java index b1dd956ebe..1bccf1ff81 100644 --- a/fcrepo-http-api/src/main/java/org/fcrepo/http/api/ContentExposingResource.java +++ b/fcrepo-http-api/src/main/java/org/fcrepo/http/api/ContentExposingResource.java @@ -154,6 +154,8 @@ import org.fcrepo.kernel.api.models.WebacAcl; import org.fcrepo.kernel.api.rdf.DefaultRdfStream; import org.fcrepo.kernel.api.rdf.RdfNamespaceRegistry; +import org.fcrepo.kernel.api.services.CreateResourceService; +import org.fcrepo.kernel.api.services.DeleteResourceService; import org.fcrepo.kernel.api.services.ManagedPropertiesService; import org.fcrepo.kernel.api.services.ReplacePropertiesService; import org.fcrepo.kernel.api.services.UpdatePropertiesService; @@ -222,6 +224,12 @@ public abstract class ContentExposingResource extends FedoraBaseResource { @Inject protected RdfNamespaceRegistry namespaceRegistry; + @Inject + protected CreateResourceService createResourceService; + + @Inject + protected DeleteResourceService deleteResourceService; + @Inject protected ReplacePropertiesService replacePropertiesService; diff --git a/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraAcl.java b/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraAcl.java index 61e4cf6ed1..f9dba14920 100644 --- a/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraAcl.java +++ b/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraAcl.java @@ -38,7 +38,6 @@ import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET; import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_WITH_CHARSET; import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X; -import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_REPOSITORY_ROOT; import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; import static org.slf4j.LoggerFactory.getLogger; @@ -46,6 +45,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; + import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.BadRequestException; @@ -74,11 +74,12 @@ import org.fcrepo.kernel.api.RdfStream; import org.fcrepo.kernel.api.exception.AccessDeniedException; import org.fcrepo.kernel.api.exception.ItemNotFoundException; +import org.fcrepo.kernel.api.exception.PathNotFoundException; import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; import org.fcrepo.kernel.api.identifiers.FedoraId; import org.fcrepo.kernel.api.models.FedoraResource; +import org.fcrepo.kernel.api.models.WebacAcl; import org.fcrepo.kernel.api.rdf.DefaultRdfStream; -import org.fcrepo.kernel.api.services.DeleteResourceService; import org.fcrepo.kernel.api.services.WebacAclService; import org.slf4j.Logger; import org.springframework.context.annotation.Scope; @@ -104,9 +105,6 @@ public class FedoraAcl extends ContentExposingResource { @PathParam("path") protected String externalPath; - @Inject - private DeleteResourceService deleteResourceService; - @Inject private WebacAclService webacAclService; @@ -130,37 +128,32 @@ public Response createFedoraWebacAcl(@HeaderParam(CONTENT_TYPE) final MediaType if (resource().isAcl() || resource().isMemento()) { throw new BadRequestException("ACL resource creation is not allowed for resource " + resource().getId()); } + LOGGER.info("PUT acl resource '{}'", externalPath()); - final boolean created; - final FedoraResource aclResource; - - final String path = toPath(translator(), externalPath); - LOGGER.info("PUT acl resource '{}'", externalPath); - - final var transaction = transaction(); - - aclResource = webacAclService.findOrCreate(transaction, path); - created = aclResource.isNew(); - final FedoraId aclId = aclResource.getFedoraId(); + final FedoraId aclId = identifierConverter().pathToInternalId(externalPath()).resolve(FCR_ACL); + final boolean exists = resourceFactory.doesResourceExist(transaction(), aclId); final MediaType contentType = requestContentType == null ? RDFMediaType.TURTLE_TYPE : valueOf(getSimpleContentType(requestContentType)); if (isRdfContentType(contentType.toString())) { - // TODO: confirm this is correct logic for ACL's - final Model model = httpRdfService.bodyToInternalModel(externalPath() + "/fcr:acl", - requestBodyStream, requestContentType, identifierConverter()); - - replacePropertiesService.perform(transaction.getId(), getUserPrincipal(), aclId, model); + final Model model = httpRdfService.bodyToInternalModel(aclId.getFullId(), + requestBodyStream, contentType, identifierConverter()); + if (exists) { + replacePropertiesService.perform(transaction().getId(), getUserPrincipal(), aclId, model); + } else { + webacAclService.create(transaction(), aclId, getUserPrincipal(), model); + } } else { throw new BadRequestException("Content-Type (" + requestContentType + ") is invalid. Try text/turtle " + "or other RDF compatible type."); } - transaction.commit(); + transaction().commitIfShortLived(); - addCacheControlHeaders(servletResponse, aclResource, transaction); + final FedoraResource aclResource = getFedoraResource(transaction(), aclId); + addCacheControlHeaders(servletResponse, aclResource, transaction()); final URI location = getUri(aclResource); - if (created) { + if (!exists) { return created(location).build(); } else { return noContent().location(location).build(); @@ -184,15 +177,17 @@ public Response updateSparql(final InputStream requestBodyStream) throw new BadRequestException("SPARQL-UPDATE requests must have content!"); } - final FedoraResource aclResource = resource().getAcl(); - - if (aclResource == null) { - if (resource().hasType(FEDORA_REPOSITORY_ROOT)) { + final FedoraId originalId = identifierConverter().pathToInternalId(externalPath()); + final FedoraId aclId = originalId.resolve(FCR_ACL); + final FedoraResource aclResource; + try { + aclResource = resourceFactory.getResource(transaction(), aclId); + } catch (final PathNotFoundException exc) { + if (originalId.isRepositoryRoot()) { throw new ClientErrorException("The default root ACL is system generated and cannot be modified. " + "To override the default root ACL you must PUT a user-defined ACL to this endpoint.", CONFLICT); } - throw new ItemNotFoundException("not found"); } @@ -206,7 +201,7 @@ public Response updateSparql(final InputStream requestBodyStream) LOGGER.info("PATCH for '{}'", externalPath); patchResourcewithSparql(aclResource, requestBody); - transaction().commit(); + transaction().commitIfShortLived(); addCacheControlHeaders(servletResponse, aclResource, transaction()); @@ -243,25 +238,29 @@ protected String externalPath() { public Response getResource() throws IOException, ItemNotFoundException { - LOGGER.info("GET resource '{}'", externalPath); + LOGGER.info("GET resource '{}'", externalPath()); - final FedoraResource aclResource = resource().getAcl(); + final FedoraId originalId = identifierConverter().pathToInternalId(externalPath()); + final FedoraId aclId = originalId.resolve(FCR_ACL); + final boolean exists = resourceFactory.doesResourceExist(transaction(), aclId); - if (aclResource == null) { - if (resource().hasType(FEDORA_REPOSITORY_ROOT)) { - final String resourceUri = getUri(resource()).toString(); - final String aclUri = resourceUri + (resourceUri.endsWith("/") ? "" : "/") + FCR_ACL; + if (!exists) { + if (originalId.isRepositoryRoot()) { + final String aclUri = identifierConverter().toExternalId(aclId.getFullId()); final RdfStream defaultRdfStream = DefaultRdfStream.fromModel(createResource(aclUri).asNode(), getDefaultAcl(aclUri)); - final RdfNamespacedStream output = new RdfNamespacedStream(defaultRdfStream, - namespaceRegistry.getNamespaces()); + final RdfStream rdfStream = httpRdfService.bodyToExternalStream(aclUri, + defaultRdfStream, identifierConverter()); + final var output = new RdfNamespacedStream( + rdfStream, namespaceRegistry.getNamespaces()); return ok(output).build(); } throw new ItemNotFoundException(String.format("No ACL found at %s", externalPath)); } + final WebacAcl aclResource = webacAclService.find(transaction(), aclId); checkCacheControlHeaders(request, servletResponse, aclResource, transaction()); LOGGER.info("GET resource '{}'", externalPath); @@ -281,24 +280,22 @@ public Response deleteObject() throws ItemNotFoundException { hasRestrictedPath(externalPath); LOGGER.info("Delete resource '{}'", externalPath); - final FedoraResource aclResource = resource().getAcl(); - if (aclResource != null) { + final FedoraId originalId = identifierConverter().pathToInternalId(externalPath()); + final FedoraId aclId = originalId.resolve(FCR_ACL); + try { + final var aclResource = resourceFactory.getResource(transaction(), aclId); deleteResourceService.perform(transaction(), aclResource, getUserPrincipal()); - } - transaction().commit(); - - if (aclResource == null) { - if (resource().hasType(FEDORA_REPOSITORY_ROOT)) { + } catch (final PathNotFoundException exc) { + if (originalId.isRepositoryRoot()) { throw new ClientErrorException("The default root ACL is system generated and cannot be deleted. " + "To override the default root ACL you must PUT a user-defined ACL to this endpoint.", CONFLICT); } - - throw new ItemNotFoundException("not found"); + throw new PathNotFoundRuntimeException(exc); + } finally { + transaction().commitIfShortLived(); } - return noContent().build(); - } /** diff --git a/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraLdp.java b/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraLdp.java index af74c86a14..0241c63a1e 100644 --- a/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraLdp.java +++ b/fcrepo-http-api/src/main/java/org/fcrepo/http/api/FedoraLdp.java @@ -109,8 +109,6 @@ import org.fcrepo.kernel.api.models.ExternalContent; import org.fcrepo.kernel.api.models.FedoraResource; import org.fcrepo.kernel.api.models.NonRdfSourceDescription; -import org.fcrepo.kernel.api.services.CreateResourceService; -import org.fcrepo.kernel.api.services.DeleteResourceService; import org.fcrepo.kernel.api.services.FixityService; import org.fcrepo.kernel.api.services.ReplaceBinariesService; import org.fcrepo.kernel.api.utils.ContentDigest; @@ -149,12 +147,6 @@ public class FedoraLdp extends ContentExposingResource { @Inject private FedoraHttpConfiguration httpConfiguration; - @Inject - private CreateResourceService createResourceService; - - @Inject - private DeleteResourceService deleteResourceService; - @Inject protected ReplaceBinariesService replaceBinariesService; diff --git a/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraAclIT.java b/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraAclIT.java index 68c215e793..c545c31ff0 100644 --- a/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraAclIT.java +++ b/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraAclIT.java @@ -53,7 +53,6 @@ import org.apache.jena.sparql.core.DatasetGraph; import org.fcrepo.http.commons.test.util.CloseableDataset; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; @@ -76,7 +75,6 @@ public void init() { subjectUri = serverAddress + id; } - @Ignore //TODO Fix this test @Test public void testCreateAclWithoutBody() throws Exception { createObjectAndClose(id); @@ -91,7 +89,6 @@ public void testCreateAclWithoutBody() throws Exception { } } - @Ignore //TODO Fix this test @Test public void testCreateAclOnAclResource() throws Exception { createObjectAndClose(id); @@ -111,7 +108,6 @@ private String createACL() throws IOException { } } - @Ignore //TODO Fix this test @Test public void testCreateAclOnBinary() throws Exception { createDatastream(id, "x", "some content"); @@ -134,7 +130,6 @@ public void testCreateAclOnBinary() throws Exception { } } - @Ignore //TODO Fix this test @Test public void testPatchAcl() throws Exception { createObjectAndClose(id); @@ -156,7 +151,6 @@ public void testPatchAcl() throws Exception { } - @Ignore //TODO Fix this test @Test public void testCreateAndRetrieveAcl() throws Exception { createObjectAndClose(id); @@ -175,7 +169,6 @@ public void testCreateAndRetrieveAcl() throws Exception { } - @Ignore //TODO Fix this test @Test public void testPutACLBadRdf() throws IOException { createObjectAndClose(id); @@ -186,7 +179,6 @@ public void testPutACLBadRdf() throws IOException { assertEquals(BAD_REQUEST.getStatusCode(), getStatus(put)); } - @Ignore //TODO Fix this test @Test public void testDeleteAcl() throws Exception { createObjectAndClose(id); @@ -219,11 +211,10 @@ public void testGetNonExistentAcl() { } - @Ignore //TODO Fix this test @Test public void testGetDefaultRootAcl() throws Exception { final String rootAclUri = serverAddress + FCR_ACL; - final String rootFedoraUri = "info:fedora/"; + final String rootFedoraUri = serverAddress; final String authzUri = rootFedoraUri + FCR_ACL + "#authz"; try (final CloseableDataset dataset = getDataset(new HttpGet(rootAclUri))) { final DatasetGraph graph = dataset.asDatasetGraph(); @@ -249,7 +240,6 @@ public void testGetDefaultRootAcl() throws Exception { } } - @Ignore //TODO Fix this test @Test public void testDeleteDefaultRootAcl() { final String rootAclUri = serverAddress + FCR_ACL; @@ -257,7 +247,6 @@ public void testDeleteDefaultRootAcl() { CONFLICT.getStatusCode(), getStatus(new HttpDelete(rootAclUri))); } - @Ignore //TODO Fix this test @Test public void testPatchDefaultRootAcl() { final String rootAclUri = serverAddress + FCR_ACL; @@ -265,7 +254,6 @@ public void testPatchDefaultRootAcl() { CONFLICT.getStatusCode(), getStatus(new HttpPatch(rootAclUri))); } - @Ignore //TODO Fix this test @Test public void testGetUserDefinedDefaultRootAcl() throws Exception { System.setProperty(ROOT_AUTHORIZATION_PROPERTY, "./target/test-classes/test-root-authorization.ttl"); @@ -284,7 +272,6 @@ public void testGetUserDefinedDefaultRootAcl() throws Exception { } } - @Ignore //TODO Fix this test @Test public void testAddModifyDeleteUserDefinedDefaultRootAcl() throws Exception { final String rootAclUri = serverAddress + FCR_ACL; @@ -316,7 +303,6 @@ public void testAddModifyDeleteUserDefinedDefaultRootAcl() throws Exception { NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(rootAclUri))); } - @Ignore //TODO Fix this test @Test public void testCreateAclWithBody() throws Exception { createObjectAndClose(id); @@ -352,7 +338,6 @@ public void testCreateAclWithBody() throws Exception { } - @Ignore //TODO Fix this test @Test public void testCreateAclWithoutAccessToSetsDefaultTarget() throws Exception { createObjectAndClose(id); @@ -387,7 +372,6 @@ public void testCreateAclWithoutAccessToSetsDefaultTarget() throws Exception { } - @Ignore //TODO Fix this test @Test public void testCreateAclWithAccessTo() throws Exception { createObjectAndClose(id); @@ -433,7 +417,6 @@ public void testCreateAclWithAccessTo() throws Exception { } } - @Ignore //TODO Fix this test @Test public void testCreateAclWithAccessToClass() throws Exception { createObjectAndClose(id); @@ -479,7 +462,6 @@ public void testCreateAclWithAccessToClass() throws Exception { } } - @Ignore //TODO Fix this test @Test public void testCreateAclWithBothAccessToandAccessToClassIsNotAllowed() throws Exception { createObjectAndClose(id); diff --git a/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraLdpIT.java b/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraLdpIT.java index 3b819d5a1d..5c8d878b50 100644 --- a/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraLdpIT.java +++ b/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/FedoraLdpIT.java @@ -721,7 +721,6 @@ public void testGetRDFSourceWithPreferMinimal() throws IOException { } @Test -@Ignore public void testCheckGetAclResourceHeaders() throws IOException { final String aclUri = createAcl(); @@ -4434,7 +4433,6 @@ public void testDeleteWithFedoraPath() throws IOException { } @Test -@Ignore public void testPostCreateNonRDFSourceWithAcl() throws IOException { final String aclURI = createAcl(); final String subjectURI = getRandomUniqueId(); @@ -4447,8 +4445,7 @@ public void testPostCreateNonRDFSourceWithAcl() throws IOException { checkResponseForMethodWithAcl(createMethod); } -@Test -@Ignore + @Test public void testPostCreateRDFSourceWithAcl() throws IOException { final String aclURI = createAcl(); final String subjectURI = getRandomUniqueId(); @@ -4462,7 +4459,6 @@ public void testPostCreateRDFSourceWithAcl() throws IOException { } @Test -@Ignore public void testPutCreateNonRDFSourceWithAcl() throws IOException { final String aclURI = createAcl(); final String subjectURI = serverAddress + getRandomUniqueId(); @@ -4475,7 +4471,6 @@ public void testPutCreateNonRDFSourceWithAcl() throws IOException { } @Test -@Ignore public void testPutCreateRDFSourceWithAcl() throws IOException { final String aclURI = createAcl(); final String subjectURI = serverAddress + getRandomUniqueId(); diff --git a/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/StateTokensIT.java b/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/StateTokensIT.java index dbb07769f5..62090a97b9 100644 --- a/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/StateTokensIT.java +++ b/fcrepo-http-api/src/test/java/org/fcrepo/integration/http/api/StateTokensIT.java @@ -63,7 +63,6 @@ public void testHeadHasStateTokenRDFSource() throws IOException { } } - @Ignore //TODO Fix this test @Test public void testAclGetHasStateTokenRDFSource() throws IOException { final String id = getRandomUniqueId(); @@ -83,7 +82,6 @@ public void testAclGetHasStateTokenRDFSource() throws IOException { } } - @Ignore //TODO Fix this test @Test public void testAclHeadHasStateTokenRDFSource() throws IOException { final String id = getRandomUniqueId(); @@ -225,7 +223,6 @@ public void testPatchWithStateTokenOnRDFSource() throws IOException { } } - @Ignore //TODO Fix this test @Test public void testPatchWithStateTokenOnAcl() throws IOException { final String id = getRandomUniqueId(); diff --git a/fcrepo-http-commons/src/main/java/org/fcrepo/http/commons/api/rdf/HttpIdentifierConverter.java b/fcrepo-http-commons/src/main/java/org/fcrepo/http/commons/api/rdf/HttpIdentifierConverter.java index c272bd2eea..c9723b6245 100644 --- a/fcrepo-http-commons/src/main/java/org/fcrepo/http/commons/api/rdf/HttpIdentifierConverter.java +++ b/fcrepo-http-commons/src/main/java/org/fcrepo/http/commons/api/rdf/HttpIdentifierConverter.java @@ -116,9 +116,15 @@ public String toExternalId(final String fedoraId) { // If it starts with our prefix, strip the prefix and any leading slashes and use it as the path // part of the URI. final String path = fedoraId.substring(FEDORA_ID_PREFIX.length()).replaceFirst("\\/", ""); - final String[] values = { path }; - // Need to pass as Array or second arg is ignored. Second arg is DON'T encode slashes - return uriBuilder().build(values, false).toString(); + final UriBuilder uri = uriBuilder(); + if (path.contains("#")) { + final String[] split = path.split("#", 2); + uri.resolveTemplate("path", split[0], false); + uri.fragment(split[1]); + } else { + uri.resolveTemplate("path", path, false); + } + return uri.build().toString(); } throw new IllegalArgumentException("Cannot translate IDs without our prefix"); } diff --git a/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/RdfLexicon.java b/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/RdfLexicon.java index b3a2a5aaac..e041f29ad3 100644 --- a/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/RdfLexicon.java +++ b/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/RdfLexicon.java @@ -42,6 +42,8 @@ public final class RdfLexicon { **/ public static final String REPOSITORY_NAMESPACE = "http://fedora.info/definitions/v4/repository#"; + public static final String REPOSITORY_WEBAC_NAMESPACE = "http://fedora.info/definitions/v4/webac#"; + public static final String FCREPO_API_NAMESPACE = "http://fedora.info/definitions/fcrepo#"; public static final String ACTIVITY_STREAMS_NAMESPACE = "https://www.w3.org/ns/activitystreams#"; @@ -218,6 +220,8 @@ public final class RdfLexicon { public static final Property WEBAC_ACCESS_TO_PROPERTY = createProperty(WEBAC_ACCESS_TO); + public static final String FEDORA_WEBAC_ACL_URI = REPOSITORY_WEBAC_NAMESPACE + "Acl"; + // Properties which are managed by the server but are not from managed namespaces private static final Set serverManagedProperties; static { diff --git a/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/identifiers/FedoraId.java b/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/identifiers/FedoraId.java index 9ac37f4ae1..9fcb19b461 100644 --- a/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/identifiers/FedoraId.java +++ b/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/identifiers/FedoraId.java @@ -19,6 +19,7 @@ import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; +import static org.fcrepo.kernel.api.FedoraTypes.FCR_TOMBSTONE; import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX; import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_LABEL_FORMATTER; @@ -27,6 +28,8 @@ import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -48,7 +51,7 @@ */ public class FedoraId { - private String id; + private String resourceId; private String fullId; private String hashUri; private boolean isRepositoryRoot = false; @@ -60,6 +63,9 @@ public class FedoraId { private String mementoDatetimeStr; private String pathOnly; + private final static Set extensions = Set.of(FCR_TOMBSTONE, FCR_METADATA, FCR_ACL, FCR_VERSIONS) + .stream().map(Pattern::compile).collect(Collectors.toSet()); + /** * Basic constructor. * @param fullId The full identifier or null if root. @@ -70,7 +76,7 @@ private FedoraId(final String fullId) { this.fullId = this.fullId.replaceAll("/+$", ""); // Carry the path of the request for any exceptions. this.pathOnly = this.fullId.substring(FEDORA_ID_PREFIX.length()); - + checkForInvalidPath(); processIdentifier(); } @@ -149,11 +155,25 @@ public String getHashUri() { } /** - * Return the ID of the base resource for this request. + * Return the ID of the base resource that exists as a separate resource. * @return the shorten id. */ public String getResourceId() { - return id; + if (isNonRdfSourceDescription) { + return resourceId + "/" + FCR_METADATA; + } else if (isAcl) { + return resourceId + "/" + FCR_ACL; + } + return resourceId; + } + + /** + * For elements that exist as separate resources but also are lifecycle tied to a resource (fcr:acl and + * fcr:metadata), return the ID of the "containing" resource. + * @return the containing resource id. + */ + public String getContainingId() { + return resourceId; } /** @@ -188,17 +208,6 @@ public String getMementoString() { return mementoDatetimeStr; } - /** - * Descriptions are needed to retrieve from the persistence, but otherwise is just an addendum to the binary. - * @return The description ID or null if not a description. - */ - public String getDescriptionId() { - if (isDescription()) { - return getResourceId() + "/" + FCR_METADATA; - } - return null; - } - /** * Resolve the string or strings against this ID to create a new one. * @@ -219,7 +228,8 @@ public FedoraId resolve(final String... addition) { } final String[] parts; if (addition[0].startsWith("/")) { - parts = Stream.of(new String[]{this.getResourceId()}, addition).flatMap(Stream::of).toArray(String[]::new); + parts = Stream.of(new String[]{this.getContainingId()}, addition).flatMap(Stream::of) + .toArray(String[]::new); return FedoraId.create(parts); } parts = Stream.of(new String[]{this.getFullId()}, addition).flatMap(Stream::of).toArray(String[]::new); @@ -295,7 +305,7 @@ private void processIdentifier() { String processID = this.fullId; if (processID.equals(FEDORA_ID_PREFIX)) { this.isRepositoryRoot = true; - this.id = this.fullId; + this.resourceId = this.fullId; // Root has no other possible endpoints, so short circuit out. return; } @@ -347,6 +357,28 @@ private void processIdentifier() { if (processID.endsWith("/")) { processID = processID.replaceAll("/+$", ""); } - this.id = processID; + this.resourceId = processID; + } + + /** + * Check for obvious path errors. + */ + private void checkForInvalidPath() { + // Check for combinations of endpoints not allowed. + if ( + // ID contains fcr:acl or fcr:tombstone AND fcr:metadata or fcr:versions + ((this.fullId.contains(FCR_ACL) || this.fullId.contains(FCR_TOMBSTONE)) && + (this.fullId.contains(FCR_METADATA) || this.fullId.contains(FCR_VERSIONS))) || + // or ID contains fcr:acl AND fcr:tombstone + (this.fullId.contains(FCR_TOMBSTONE) && this.fullId.contains(FCR_ACL)) + ) { + throw new InvalidResourceIdentifierException(String.format("Path is invalid: %s", pathOnly)); + } + // Ensure we don't have 2 of any of the extensions, ie. info:fedora/object/fcr:acl/fcr:acl, etc. + for (final Pattern extension : extensions) { + if (extension.matcher(this.fullId).results().count() > 1) { + throw new InvalidResourceIdentifierException(String.format("Path is invalid: %s", pathOnly)); + } + } } } diff --git a/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/services/WebacAclService.java b/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/services/WebacAclService.java index 112b1a0129..c4c6370c67 100644 --- a/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/services/WebacAclService.java +++ b/fcrepo-kernel-api/src/main/java/org/fcrepo/kernel/api/services/WebacAclService.java @@ -18,13 +18,37 @@ package org.fcrepo.kernel.api.services; +import org.apache.jena.rdf.model.Model; +import org.fcrepo.kernel.api.Transaction; +import org.fcrepo.kernel.api.identifiers.FedoraId; import org.fcrepo.kernel.api.models.WebacAcl; /** * Service for creating and retrieving {@link WebacAcl} * * @author peichman + * @author whikloj * @since 6.0.0 */ -public interface WebacAclService extends Service { +public interface WebacAclService { + + /** + * Retrieve an existing WebACL by transaction and path + * + * @param fedoraId the fedoraID to the resource the ACL is part of + * @param transaction the transaction + * @return retrieved ACL + */ + WebacAcl find(final Transaction transaction, final FedoraId fedoraId); + + /** + * Retrieve or create a new WebACL by transaction and path + * + * @param transaction the transaction + * @param fedoraId the fedoraID to the resource the ACL is part of + * @param userPrincipal the user creating the ACL. + * @param model the contents of the ACL RDF. + */ + void create(final Transaction transaction, final FedoraId fedoraId, final String userPrincipal, + final Model model); } diff --git a/fcrepo-kernel-api/src/test/java/org/fcrepo/kernel/api/identifiers/FedoraIdTest.java b/fcrepo-kernel-api/src/test/java/org/fcrepo/kernel/api/identifiers/FedoraIdTest.java index 974e98ba3b..4a6afff64f 100644 --- a/fcrepo-kernel-api/src/test/java/org/fcrepo/kernel/api/identifiers/FedoraIdTest.java +++ b/fcrepo-kernel-api/src/test/java/org/fcrepo/kernel/api/identifiers/FedoraIdTest.java @@ -121,11 +121,10 @@ public void testNormalMementoException2() throws Exception { final FedoraId fedoraID = FedoraId.create(testID); } - @Test + @Test(expected = InvalidResourceIdentifierException.class) public void testMetadataAcl() throws Exception { final String testID = FEDORA_ID_PREFIX + "/first-object/" + FCR_METADATA + "/" + FCR_ACL; final FedoraId fedoraID = FedoraId.create(testID); - assertResource(fedoraID, Arrays.asList("ACL", "METADATA"), testID, FEDORA_ID_PREFIX + "/first-object"); } @@ -188,7 +187,7 @@ public void testMetadataWithHash() throws Exception { assertEquals("hashURI", fedoraID.getHashUri()); } - @Test + @Test(expected = InvalidResourceIdentifierException.class) public void testMetadataAclWithHash() throws Exception { final String testID = FEDORA_ID_PREFIX + "/first-object/" + FCR_METADATA + "/" + FCR_ACL + "#hashURI"; final FedoraId fedoraID = FedoraId.create(testID); @@ -338,6 +337,11 @@ public void testResolveEmptyString() { fedoraId.resolve(""); } + @Test(expected = InvalidResourceIdentifierException.class) + public void testDoubleAcl() { + final FedoraId fedoraId = FedoraId.create("core-object/" + FCR_ACL).resolve(FCR_ACL); + } + /** * Utility to test a FedoraId against expectations. @@ -390,6 +394,6 @@ private void assertResource(final FedoraId fedoraID, final List type, fi assertFalse(fedoraID.isTimemap()); } assertEquals(fullID, fedoraID.getFullId()); - assertEquals(shortID, fedoraID.getResourceId()); + assertEquals(shortID, fedoraID.getContainingId()); } } diff --git a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/ContainmentIndexImpl.java b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/ContainmentIndexImpl.java index d2559b45f0..bd5e993eeb 100644 --- a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/ContainmentIndexImpl.java +++ b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/ContainmentIndexImpl.java @@ -385,7 +385,8 @@ public void rollbackTransaction(final Transaction tx) { @Override public boolean resourceExists(final String txID, final FedoraId fedoraID) { - final String resourceID = fedoraID.getResourceId(); + // Get the containing ID because fcr:metadata will not exist here but MUST exist if the containing resource does + final String resourceID = fedoraID.getContainingId(); LOGGER.debug("Checking if {} exists in transaction {}", resourceID, txID); if (fedoraID.isRepositoryRoot()) { // Root always exists. diff --git a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/NonRdfSourceDescriptionImpl.java b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/NonRdfSourceDescriptionImpl.java index fb05f0210a..08adcfc8b6 100644 --- a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/NonRdfSourceDescriptionImpl.java +++ b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/NonRdfSourceDescriptionImpl.java @@ -58,13 +58,13 @@ public NonRdfSourceDescriptionImpl(final FedoraId fedoraID, @Override public String getId() { - return getFedoraId().getDescriptionId(); + return getFedoraId().getResourceId(); } @Override public FedoraResource getDescribedResource() { // Get a FedoraId for the binary - final FedoraId describedId = FedoraId.create(this.getFedoraId().getResourceId()); + final FedoraId describedId = FedoraId.create(this.getFedoraId().getContainingId()); try { return this.resourceFactory.getResource(tx, describedId); } catch (final PathNotFoundException e) { diff --git a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/ResourceFactoryImpl.java b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/ResourceFactoryImpl.java index a63f702df3..3fea113e38 100644 --- a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/ResourceFactoryImpl.java +++ b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/ResourceFactoryImpl.java @@ -40,6 +40,7 @@ import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER; import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER; import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI; +import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_WEBAC_ACL_URI; import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER; import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; import static org.slf4j.LoggerFactory.getLogger; @@ -96,8 +97,9 @@ public boolean doesResourceExist(final Transaction transaction, final FedoraId f // Root always exists. return true; } - if (!fedoraId.isMemento()) { - // containment index doesn't handle versions, so don't bother checking for them. + if (!(fedoraId.isMemento() || fedoraId.isAcl())) { + // containment index doesn't handle versions and only tells us if the resource (not acl) is there, + // so don't bother checking for them. final String transactionId = transaction == null ? null : transaction.getId(); return containmentIndex.resourceExists(transactionId, fedoraId); } else { @@ -105,9 +107,10 @@ public boolean doesResourceExist(final Transaction transaction, final FedoraId f final PersistentStorageSession psSession = getSession(transaction); try { - final String id = fedoraId.isDescription() ? fedoraId.getDescriptionId() : fedoraId.getResourceId(); - psSession.getHeaders(id, fedoraId.getMementoInstant()); - return true; + // Resource ID for metadata or ACL contains their individual endopoints (ie. fcr:metadata, fcr:acl) + final String id = fedoraId.getResourceId(); + final ResourceHeaders headers = psSession.getHeaders(id, fedoraId.getMementoInstant()); + return !headers.isDeleted(); } catch (final PersistentItemNotFoundException e) { // Object doesn't exist. return false; @@ -145,6 +148,9 @@ private Class getClassForTypes(final ResourceHeade if (FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI.equals(ixModel)) { return NonRdfSourceDescriptionImpl.class; } + if (FEDORA_WEBAC_ACL_URI.equals(ixModel)) { + return WebacAclImpl.class; + } // TODO add the rest of the types throw new ResourceTypeException("Could not identify the resource type for interaction model " + ixModel); } @@ -161,14 +167,14 @@ private FedoraResource instantiateResource(final Transaction transaction, final FedoraId identifier) throws PathNotFoundException { try { - // For descriptions we need the fcr:metadata part so we get the description ID. - final String id = identifier.isDescription() ? identifier.getDescriptionId() : identifier.getResourceId(); + // For descriptions and ACLs we need the actual endpoint. + final String id = identifier.getResourceId(); final var psSession = getSession(transaction); final Instant versionDateTime = identifier.isMemento() ? identifier.getMementoInstant() : null; final var headers = psSession.getHeaders(id, versionDateTime); if (headers.isDeleted()) { - final var rootId = FedoraId.create(identifier.getResourceId()); + final var rootId = FedoraId.create(identifier.getContainingId()); final var tombstone = new TombstoneImpl(rootId, transaction, persistentStorageSessionManager, this); tombstone.setLastModifiedDate(headers.getLastModifiedDate()); diff --git a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/WebacAclImpl.java b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/WebacAclImpl.java new file mode 100644 index 0000000000..60f822fdc6 --- /dev/null +++ b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/models/WebacAclImpl.java @@ -0,0 +1,65 @@ +/* + * Licensed to DuraSpace under one or more contributor license agreements. + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * DuraSpace licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fcrepo.kernel.impl.models; + +import org.fcrepo.kernel.api.Transaction; +import org.fcrepo.kernel.api.exception.PathNotFoundException; +import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; +import org.fcrepo.kernel.api.identifiers.FedoraId; +import org.fcrepo.kernel.api.models.FedoraResource; +import org.fcrepo.kernel.api.models.ResourceFactory; +import org.fcrepo.kernel.api.models.WebacAcl; +import org.fcrepo.persistence.api.PersistentStorageSessionManager; + +/** + * Webac Acl class + */ +public class WebacAclImpl extends ContainerImpl implements WebacAcl { + + /** + * Constructor + * @param fedoraID the internal identifier + * @param tx the current transaction + * @param pSessionManager a session manager + * @param resourceFactory a resource factory instance. + */ + public WebacAclImpl(final FedoraId fedoraID, final Transaction tx, + final PersistentStorageSessionManager pSessionManager, final ResourceFactory resourceFactory) { + super(fedoraID, tx, pSessionManager, resourceFactory); + } + + @Override + public FedoraResource getDescribedResource() { + final var originalId = FedoraId.create(getFedoraId().getContainingId()); + try { + return resourceFactory.getResource(tx, originalId); + } catch (final PathNotFoundException exc) { + throw new PathNotFoundRuntimeException(exc); + } + } + + @Override + public boolean isOriginalResource() { + return false; + } + + @Override + public boolean isAcl() { + return true; + } +} diff --git a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/AbstractService.java b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/AbstractService.java index 8ab1edc9c8..799218ee2b 100644 --- a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/AbstractService.java +++ b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/AbstractService.java @@ -218,10 +218,9 @@ protected void ensureValidMemberRelation(final Model inputModel) { * - Throws an exception if an authorization has both accessTo and accessToClass * - Adds a default accessTo target if an authorization has neither accessTo nor accessToClass * - * @param fedoraId the fedora Id * @param inputModel to be checked and updated */ - protected void ensureValidACLAuthorization(final String fedoraId, final Model inputModel) { + protected void ensureValidACLAuthorization(final Model inputModel) { // TODO -- check ACL first @@ -241,7 +240,7 @@ protected void ensureValidACLAuthorization(final String fedoraId, final Model in throw new ACLAuthorizationConstraintViolationException( String.format( "Using both accessTo and accessToClass within " + - "a single Authorization is not allowed: {0}.", + "a single Authorization is not allowed: %s.", subject.toString().substring(subject.toString().lastIndexOf("#")))); } else if (!(graph.contains(subject, WEBAC_ACCESS_TO_URI, Node.ANY) || graph.contains(subject, WEBAC_ACCESS_TO_CLASS_URI, Node.ANY))) { diff --git a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/ReplacePropertiesServiceImpl.java b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/ReplacePropertiesServiceImpl.java index 4cb591852a..5ca20d8367 100644 --- a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/ReplacePropertiesServiceImpl.java +++ b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/ReplacePropertiesServiceImpl.java @@ -59,7 +59,7 @@ public void perform(final String txId, ensureValidMemberRelation(inputModel); - ensureValidACLAuthorization(fedoraId.getFullId(), inputModel); + ensureValidACLAuthorization(inputModel); checkForSmtsLdpTypes(inputModel); diff --git a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/WebacAclServiceImpl.java b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/WebacAclServiceImpl.java index 1e9c8047da..0f12dc9ccb 100644 --- a/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/WebacAclServiceImpl.java +++ b/fcrepo-kernel-impl/src/main/java/org/fcrepo/kernel/impl/services/WebacAclServiceImpl.java @@ -17,9 +17,27 @@ */ package org.fcrepo.kernel.impl.services; +import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_WEBAC_ACL_URI; +import static org.fcrepo.kernel.api.rdf.DefaultRdfStream.fromModel; + +import javax.inject.Inject; + +import org.apache.jena.rdf.model.Model; +import org.fcrepo.kernel.api.RdfStream; import org.fcrepo.kernel.api.Transaction; +import org.fcrepo.kernel.api.exception.PathNotFoundException; +import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; +import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; +import org.fcrepo.kernel.api.identifiers.FedoraId; +import org.fcrepo.kernel.api.models.ResourceFactory; import org.fcrepo.kernel.api.models.WebacAcl; +import org.fcrepo.kernel.api.operations.RdfSourceOperation; +import org.fcrepo.kernel.api.operations.RdfSourceOperationFactory; import org.fcrepo.kernel.api.services.WebacAclService; +import org.fcrepo.kernel.impl.models.WebacAclImpl; +import org.fcrepo.persistence.api.PersistentStorageSession; +import org.fcrepo.persistence.api.PersistentStorageSessionManager; +import org.fcrepo.persistence.api.exceptions.PersistentStorageException; import org.springframework.stereotype.Component; /** @@ -30,18 +48,46 @@ @Component public class WebacAclServiceImpl extends AbstractService implements WebacAclService { - @Override - public boolean exists(final Transaction transaction, final String path) { - return false; - } + @Inject + private PersistentStorageSessionManager psManager; + + @Inject + private ResourceFactory resourceFactory; + + @Inject + private RdfSourceOperationFactory rdfSourceOperationFactory; @Override - public WebacAcl find(final Transaction transaction, final String path) { - return null; + public WebacAcl find(final Transaction transaction, final FedoraId fedoraId) { + try { + return resourceFactory.getResource(transaction, fedoraId, WebacAclImpl.class); + } catch (final PathNotFoundException exc) { + throw new PathNotFoundRuntimeException(exc); + } } @Override - public WebacAcl findOrCreate(final Transaction transaction, final String path) { - return null; + public void create(final Transaction transaction, final FedoraId fedoraId, final String userPrincipal, + final Model model) { + final PersistentStorageSession pSession = this.psManager.getSession(transaction.getId()); + + ensureValidACLAuthorization(model); + + final RdfStream stream = fromModel(model.getResource(fedoraId.getFullId()).asNode(), model); + + final RdfSourceOperation createOp = rdfSourceOperationFactory + .createBuilder(fedoraId.getResourceId(), FEDORA_WEBAC_ACL_URI) + .parentId(fedoraId.getContainingId()) + .triples(stream) + .relaxedProperties(model) + .userPrincipal(userPrincipal) + .build(); + try { + pSession.persist(createOp); + recordEvent(transaction.getId(), fedoraId, createOp); + } catch (final PersistentStorageException exc) { + throw new RepositoryRuntimeException(String.format("failed to create resource %s", fedoraId), exc); + } } + }