diff --git a/deploy/src/main.ts b/deploy/src/main.ts index 31b9ee4..41300bd 100644 --- a/deploy/src/main.ts +++ b/deploy/src/main.ts @@ -427,6 +427,13 @@ ds_writer.write({ Repo: { crud: { ...H_CRUD_DEFAULT, + Update: { + implies: [ + 'ReadRepo', + 'UpdateBranch', // PATCH for updating repo metadata + 'UpdateLock', // PATCH for updating repo metadata + ], + }, Delete: { implies: [ 'UpdateRepo', @@ -446,13 +453,7 @@ ds_writer.write({ }, Lock: { - crud: { - Create: {}, - Read: {}, - Delete: { - implies: ['ReadLock'], - }, - }, + crud: H_CRUD_DEFAULT, }, AccessControlAny: { @@ -521,4 +522,4 @@ ds_writer.write({ }), }, }, -}) \ No newline at end of file +}) diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt b/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt index 7a10640..badbfe2 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt @@ -150,6 +150,11 @@ val LOCK_CRUD_CONDITIONS = REPO_CRUD_CONDITIONS.append { } } +val LOCK_UPDATE_CONDITIONS = LOCK_CRUD_CONDITIONS.append { + // require that the user has the ability to update locks on a lock-level scope + permit(Permission.UPDATE_LOCK, Scope.LOCK) +} + enum class ConditionType { INSPECT, REQUIRE, diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt b/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt index f4956bf..64c4549 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt @@ -81,3 +81,7 @@ open class Http500Excpetion(msg: String): HttpException(msg, HttpStatusCode.Inte class ServerBugException(msg: String?=null): Http500Excpetion("Possible server implementation bug: ${msg?: "(no description)"}") + +open class Http501Exception(msg: String): HttpException(msg, HttpStatusCode.NotImplemented) + +class NotImplementedException(msg: String): Http501Exception("That action is not yet implemented. $msg") diff --git a/src/main/kotlin/org/openmbee/flexo/mms/General.kt b/src/main/kotlin/org/openmbee/flexo/mms/General.kt new file mode 100644 index 0000000..744c9dd --- /dev/null +++ b/src/main/kotlin/org/openmbee/flexo/mms/General.kt @@ -0,0 +1,14 @@ +package org.openmbee.flexo.mms + +import io.ktor.http.* +import io.ktor.server.response.* +import org.openmbee.flexo.mms.server.GenericResponse +import org.openmbee.flexo.mms.server.LdpDcLayer1Context + +suspend fun LdpDcLayer1Context.notImplemented() { + call.respondText( + "That operation is not yet implemented", + ContentType.Text.Plain, + HttpStatusCode.NotImplemented + ) +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt b/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt index 35286d6..66b3213 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt @@ -156,6 +156,9 @@ fun prefixesFor( ) } } + else { + add("morl" to MMS_URNS.never) + } if(null !== diffId) { with("$this/diffs/$diffId") { @@ -164,6 +167,9 @@ fun prefixesFor( ) } } + else { + add("morl" to MMS_URNS.never) + } if(null != lockId) { with("$this/locks/$lockId") { @@ -172,6 +178,9 @@ fun prefixesFor( ) } } + else { + add("morl" to MMS_URNS.never) + } if(null != commitId) { with("$this/commits/$commitId") { @@ -181,6 +190,9 @@ fun prefixesFor( ) } } + else { + add("morl" to MMS_URNS.never) + } } } } @@ -202,7 +214,7 @@ fun prefixesFor( object MMS { private val BASE = SPARQL_PREFIXES["mms"]!! val uri = BASE - + private fun res(id: String): Resource { return ResourceFactory.createResource("${BASE}${id}") } @@ -234,7 +246,7 @@ object MMS { // object properties val id = ResourceFactory.createProperty(BASE, "id") - + private fun prop(id: String): Property { return ResourceFactory.createProperty(BASE, id) } @@ -343,6 +355,8 @@ object MMS_OBJECT { object MMS_URNS { private val mms = "urn:mms" + val never = "$mms:never" + object SUBJECT { val aggregator = "$mms:aggregator" val auth = "$mms:auth" @@ -354,4 +368,4 @@ object MMS_URNS { val pass = "$mms:pass" val policy = "$mms:policy" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/Branches.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/Branches.kt index e450271..27f6246 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/Branches.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/Branches.kt @@ -3,6 +3,7 @@ package org.openmbee.flexo.mms.routes import io.ktor.server.routing.* import org.openmbee.flexo.mms.BRANCH_UPDATE_CONDITIONS import org.openmbee.flexo.mms.guardedPatch +import org.openmbee.flexo.mms.notImplemented import org.openmbee.flexo.mms.routes.ldp.createBranch import org.openmbee.flexo.mms.routes.ldp.getBranches import org.openmbee.flexo.mms.routes.ldp.headBranches @@ -44,6 +45,9 @@ fun Route.crudBranches() { // create new branch createBranch(usedPost=true) } + + // method not allowed + otherwiseNotAllowed() } // specific branch @@ -80,5 +84,13 @@ fun Route.crudBranches() { preconditions = BRANCH_UPDATE_CONDITIONS, ) } + + // delete not yet implemented + delete { + notImplemented() + } + + // method not allowed + otherwiseNotAllowed() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/Locks.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/Locks.kt index 582a6a6..2a42a46 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/Locks.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/Locks.kt @@ -1,6 +1,10 @@ package org.openmbee.flexo.mms.routes import io.ktor.server.routing.* +import org.openmbee.flexo.mms.LOCK_UPDATE_CONDITIONS +import org.openmbee.flexo.mms.NotImplementedException +import org.openmbee.flexo.mms.guardedPatch +import org.openmbee.flexo.mms.reindent import org.openmbee.flexo.mms.routes.ldp.createOrReplaceLock import org.openmbee.flexo.mms.routes.ldp.deleteLock import org.openmbee.flexo.mms.routes.ldp.getLocks @@ -22,6 +26,16 @@ fun Route.crudLocks() { } } + // state of a lock + head { + headLocks(true) + } + + // get all locks + get { + getLocks(true) + } + // create new lock post { slug -> // set policy id on context @@ -30,6 +44,9 @@ fun Route.crudLocks() { // create new lock createOrReplaceLock() } + + // method not allowed + otherwiseNotAllowed("locks") } // specific lock @@ -57,9 +74,38 @@ fun Route.crudLocks() { createOrReplaceLock() } + // update lock metadata + patch { + // build conditions + val localConditions = LOCK_UPDATE_CONDITIONS.append { + // enforce preconditions if present + appendPreconditions { values -> + """ + graph mor-graph:Metadata { + morl: mms:etag ?__mms_etag . + + ${values.reindent(6)} + } + """ + } + } + + // handle all varieties of accepted PATCH request formats + guardedPatch( + updateRequest = it, + objectKey = "morl", + graph = "mor-graph:Metadata", + preconditions = localConditions, + ) + } + // delete a lock delete { - deleteLock() +// deleteLock() + throw NotImplementedException("DELETE Lock") } + + // method not allowed + otherwiseNotAllowed("lock") } } diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/LockWrite.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/LockWrite.kt index 8948466..45ac308 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/LockWrite.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/LockWrite.kt @@ -6,6 +6,7 @@ import org.apache.jena.vocabulary.RDF import org.openmbee.flexo.mms.* import org.openmbee.flexo.mms.server.LdpDcLayer1Context import org.openmbee.flexo.mms.server.LdpMutateResponse +import org.openmbee.flexo.mms.server.LdpPatchResponse // require that the given lock does not exist (before attempting to create it) @@ -180,7 +181,7 @@ suspend fun LdpDcLayer1Context foaf:homepage . } where { <> dct:title "$demoBranchName"@en . } - """.trimIndent())) + """.trimIndent() + ) + ) }.apply { response includesTriples { subject(localIri(demoBranchPath)) { @@ -35,5 +40,31 @@ class BranchUpdate : RefAny() { } } } + + "all branches rejects other methods" { + createBranch(demoRepoPath, "master", demoBranchId, demoBranchName) + + withTest { + onlyAllowsMethods("$demoRepoPath/branches", setOf( + HttpMethod.Head, + HttpMethod.Get, + HttpMethod.Post, + )) + } + } + + "branch rejects other methods" { + createBranch(demoRepoPath, "master", demoBranchId, demoBranchName) + + withTest { + onlyAllowsMethods(demoBranchPath, setOf( + HttpMethod.Head, + HttpMethod.Get, + HttpMethod.Put, + HttpMethod.Patch, + HttpMethod.Delete, + )) + } + } } } diff --git a/src/test/kotlin/org/openmbee/flexo/mms/LockAny.kt b/src/test/kotlin/org/openmbee/flexo/mms/LockAny.kt index 7d115ea..c3648cb 100644 --- a/src/test/kotlin/org/openmbee/flexo/mms/LockAny.kt +++ b/src/test/kotlin/org/openmbee/flexo/mms/LockAny.kt @@ -4,23 +4,25 @@ import io.kotest.matchers.string.shouldNotBeBlank import io.ktor.http.* import io.ktor.server.testing.* import org.apache.jena.vocabulary.RDF -import org.openmbee.flexo.mms.util.TriplesAsserter -import org.openmbee.flexo.mms.util.exactly -import org.openmbee.flexo.mms.util.iri -import org.openmbee.flexo.mms.util.startsWith +import org.openmbee.flexo.mms.util.* import org.slf4j.LoggerFactory // validates response triples for a lock -fun TriplesAsserter.validateLockTriples(lockId: String, etag: String) { +fun TriplesAsserter.validateLockTriples( + lockId: String, + etag: String?=null, + extraPatterns: List = listOf() +) { // lock triples subjectTerse("mor-lock:$lockId") { exclusivelyHas( RDF.type exactly MMS.Lock, MMS.id exactly lockId, - MMS.etag exactly etag, + if(etag != null) MMS.etag exactly etag else MMS.etag startsWith "", MMS.commit startsWith model.expandPrefix("mor-commit:").iri, MMS.snapshot startsWith model.expandPrefix("mor-snapshot:Model.").iri, MMS.createdBy exactly model.expandPrefix("mu:").iri, + *extraPatterns.toTypedArray(), ) } } diff --git a/src/test/kotlin/org/openmbee/flexo/mms/LockLdpDc.kt b/src/test/kotlin/org/openmbee/flexo/mms/LockLdpDc.kt index 9d68640..2917c54 100644 --- a/src/test/kotlin/org/openmbee/flexo/mms/LockLdpDc.kt +++ b/src/test/kotlin/org/openmbee/flexo/mms/LockLdpDc.kt @@ -21,6 +21,8 @@ class LockLdpDc : LockAny() { // // validate // validateCreatedLockTriples(demoLockId, etag, demoOrgPath) // } + + patch() } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/org/openmbee/flexo/mms/LockRead.kt b/src/test/kotlin/org/openmbee/flexo/mms/LockRead.kt index ddb0fcb..abb718f 100644 --- a/src/test/kotlin/org/openmbee/flexo/mms/LockRead.kt +++ b/src/test/kotlin/org/openmbee/flexo/mms/LockRead.kt @@ -1,7 +1,9 @@ package org.openmbee.flexo.mms +import io.kotest.matchers.shouldBe import io.ktor.http.* +import io.ktor.server.request.* import org.openmbee.flexo.mms.util.* class LockRead : LockAny() { @@ -9,10 +11,17 @@ class LockRead : LockAny() { listOf( "head", "get", + "patch", +// "delete", ).forEach { method -> "$method non-existent lock" { withTest { - httpRequest(HttpMethod(method.uppercase()), demoLockPath) {}.apply { + httpRequest(HttpMethod(method.uppercase()), demoLockPath) { + // PATCH request + if(method == "patch") { + addHeader("Content-Type", RdfContentTypes.Turtle.toString()) + } + }.apply { response shouldHaveStatus HttpStatusCode.NotFound } } @@ -25,6 +34,7 @@ class LockRead : LockAny() { withTest { httpHead(demoLockPath) {}.apply { response shouldHaveStatus HttpStatusCode.NoContent + response.content.shouldBe(null) } } } @@ -41,5 +51,51 @@ class LockRead : LockAny() { } } } + + "lock other methods not allowed" { + withTest { + onlyAllowsMethods(demoLockPath, setOf( + HttpMethod.Head, + HttpMethod.Get, + HttpMethod.Put, + HttpMethod.Patch, + HttpMethod.Delete, + )) + } + } + + "head all locks" { + createLock(demoRepoPath, masterBranchPath, demoLockId) + + withTest { + httpHead("$demoRepoPath/locks") {}.apply { + response shouldHaveStatus HttpStatusCode.NoContent + response.content.shouldBe(null) + } + } + } + + "get all locks" { + createLock(demoRepoPath, masterBranchPath, demoLockId) + + withTest { + httpGet("$demoRepoPath/locks") {}.apply { + response shouldHaveStatus HttpStatusCode.OK + response includesTriples { + validateLockTriples(demoLockId) + } + } + } + } + + "all locks other methods not allowed" { + withTest { + onlyAllowsMethods("$demoRepoPath/locks", setOf( + HttpMethod.Head, + HttpMethod.Get, + HttpMethod.Post, + )) + } + } } } diff --git a/src/test/kotlin/org/openmbee/flexo/mms/LockWrite.kt b/src/test/kotlin/org/openmbee/flexo/mms/LockWrite.kt new file mode 100644 index 0000000..b9e0878 --- /dev/null +++ b/src/test/kotlin/org/openmbee/flexo/mms/LockWrite.kt @@ -0,0 +1,52 @@ +package org.openmbee.flexo.mms + +import io.ktor.http.* +import org.apache.jena.vocabulary.DCTerms +import org.openmbee.flexo.mms.util.* + + +class LockWrite : LockAny() { + init { + "patch lock with TTL" { + createLock(demoRepoPath, masterBranchPath, demoLockId) + + withTest { + httpPatch(demoLockPath) { + setTurtleBody(withAllTestPrefixes(""" + <> dct:description "foo" . + """.trimIndent())) + }.apply { + response shouldHaveStatus HttpStatusCode.OK + + response includesTriples { + validateLockTriples(demoLockId, null, listOf( + DCTerms.description exactly "foo" + )) + } + } + } + } + + "patch lock with SPARQL UPDATE" { + createLock(demoRepoPath, masterBranchPath, demoLockId) + + withTest { + httpPatch(demoLockPath) { + setSparqlUpdateBody(withAllTestPrefixes(""" + insert data { + <> dct:description "foo" . + } + """.trimIndent())) + }.apply { + response shouldHaveStatus HttpStatusCode.OK + + response includesTriples { + validateLockTriples(demoLockId, null, listOf( + DCTerms.description exactly "foo" + )) + } + } + } + } + } +}