Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add head/get for locks gsp #170

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/kotlin/org/openmbee/flexo/mms/routes/Locks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ fun Route.crudLocks() {
deleteLock()
}
}
}
}
24 changes: 22 additions & 2 deletions src/main/kotlin/org/openmbee/flexo/mms/routes/Model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,31 @@ package org.openmbee.flexo.mms.routes.sparql
import com.linkedin.migz.MiGzOutputStream
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import loadModel
import org.openmbee.flexo.mms.*
import org.openmbee.flexo.mms.routes.gsp.RefType
import org.openmbee.flexo.mms.server.graphStoreProtocol
import org.openmbee.flexo.mms.routes.gsp.readModel
import java.io.ByteArrayOutputStream



/**
* Model CRUD routing
*/
fun Route.crudModel() {
// by branch
graphStoreProtocol("/orgs/{orgId}/repos/{repoId}/branches/{branchId}/graph") {
// 5.6 HEAD: check state of graph
head {
readModel()
readModel(RefType.BRANCH)
}

// 5.2 GET: read graph
get {
readModel()
readModel(RefType.BRANCH)
}

// 5.3 PUT: overwrite (load)
Expand All @@ -46,6 +50,22 @@ fun Route.crudModel() {
//
// }
}

// by lock
graphStoreProtocol("/orgs/{orgId}/repos/{repoId}/locks/{lockId}/graph") {
// 5.6 HEAD: check state of graph
head {
readModel(RefType.LOCK)
}

// 5.2 GET: read graph
get {
readModel(RefType.LOCK)
}

// otherwise, deny the method
otherwiseNotAllowed("locks")
}
}


Expand Down
31 changes: 26 additions & 5 deletions src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package org.openmbee.flexo.mms.routes.gsp

import com.concurrentli.ManagedMultiBlocker.block
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import org.openmbee.flexo.mms.*
import org.openmbee.flexo.mms.server.GspLayer1Context
import org.openmbee.flexo.mms.server.GspReadResponse

suspend fun GspLayer1Context<GspReadResponse>.readModel() {
enum class RefType {
BRANCH,
LOCK,
}

suspend fun GspLayer1Context<GspReadResponse>.readModel(refType: RefType) {
parsePathParams {
org()
repo()
branch()
when(refType) {
RefType.BRANCH -> branch()
RefType.LOCK -> lock()
}
}

val authorizedIri = "<${MMS_URNS.SUBJECT.auth}:${transactionId}>"
Expand All @@ -24,11 +34,17 @@ suspend fun GspLayer1Context<GspReadResponse>.readModel() {
""")
}
where {
auth(Permission.READ_BRANCH.scope.id, BRANCH_QUERY_CONDITIONS)
when(refType) {
RefType.BRANCH -> auth(Permission.READ_BRANCH.scope.id, BRANCH_QUERY_CONDITIONS)
RefType.LOCK -> auth(Permission.READ_LOCK.scope.id, LOCK_QUERY_CONDITIONS)
}

raw("""
graph mor-graph:Metadata {
morb: mms:commit/^mms:commit ?ref .
${when(refType) {
RefType.BRANCH -> "morb:"
RefType.LOCK -> "morl:"
}} mms:commit/^mms:commit ?ref .

?ref mms:snapshot ?modelSnapshot .

Expand Down Expand Up @@ -59,9 +75,14 @@ suspend fun GspLayer1Context<GspReadResponse>.readModel() {
throw Http403Exception(this, call.request.path())
}
}
// HEAD method
else if(call.request.httpMethod == HttpMethod.Head) {
call.respond(HttpStatusCode.OK)
}
// GET
else {
// try to avoid parsing model for performance reasons
val modelText = constructResponseText.replace("""$authorizedIri\s+<${MMS_URNS.PREDICATE.policy}>\s+\"(user|group)\"\s+\.""".toRegex(), "")
val modelText = constructResponseText.replace("""$authorizedIri\s+<${MMS_URNS.PREDICATE.policy}>\s+"(user|group)"\s+\.""".toRegex(), "")

call.respondText(modelText, contentType=RdfContentTypes.Turtle)
}
Expand Down
83 changes: 74 additions & 9 deletions src/main/kotlin/org/openmbee/flexo/mms/server/Protocol.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
protected val acceptableMediaTypesForPut: List<ContentType>?=acceptableMediaTypesForPost,
) {
// allowed HTTP methods for a resource matching the given route
protected var allowedMethods = mutableListOf("Options")
protected var allowedMethods = mutableListOf(HttpMethod.Options)

// accepted content types for a POST request to a resource matching the given route
protected var acceptedTypesPost = mutableListOf<ContentType>()
Expand Down Expand Up @@ -127,6 +127,24 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
layer1: Layer1Context<TRequestContext, TResponseContext>
) {}

protected suspend fun checkAllowed(
call: ApplicationCall,
resourcesLabel: String?=null
) {
// set allowed methods
call.response.headers.append("Allow", allowedMethods.joinToString(", ") { it.value })

// method not allowed
if(!allowedMethods.contains(call.request.httpMethod)) {
// reject request
call.respondText(
"${call.request.httpMethod.value} method not allowed on ${resourcesLabel ?: "this resource"}",
ContentType.Text.Plain,
HttpStatusCode.MethodNotAllowed
)
}
}

// applies custom before each and handles common
protected suspend fun <TResponseContext: GenericResponse> eachCall(
call: ApplicationCall,
Expand All @@ -144,8 +162,8 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
// invoke custom beforeEach if defined
beforeEach?.invoke(layer1)

// set allowed methods
call.response.headers.append("Allow", allowedMethods.joinToString { ", " })
// check allowed methods
checkAllowed(call)

// set accepted RDF formats for POST requests
if(acceptedTypesPost.isNotEmpty()) {
Expand Down Expand Up @@ -186,7 +204,7 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
called: ((Layer1Context<TRequestContext, TResponseContext>) -> Unit)?=null
) {
// add to allowed methods
allowedMethods.add("HEAD")
allowedMethods.add(HttpMethod.Head)

// allow implementor to handle when head is declared
declaredHead()
Expand All @@ -211,7 +229,7 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
called: ((Layer1Context<TRequestContext, TResponseContext>) -> Unit)?=null
) {
// add to allowed methods
allowedMethods.add("GET")
allowedMethods.add(HttpMethod.Get)

// allow implementor to handle when get is declared
declaredGet()
Expand All @@ -236,7 +254,7 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
called: ((Layer1Context<TRequestContext, TResponseContext>, slug: String) -> Unit)?=null
) {
// add to allowed methods
allowedMethods.add("POST")
allowedMethods.add(HttpMethod.Post)

// add supported RDF content types to accepted POST types
acceptedTypesPost.addAll(acceptableMediaTypesForPost)
Expand Down Expand Up @@ -272,7 +290,7 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
called: ((Layer1Context<TRequestContext, TResponseContext>) -> Unit)?=null
) {
// add to allowed methods
allowedMethods.add("PUT")
allowedMethods.add(HttpMethod.Put)

// add supported RDF content types to accepted PUT types
acceptedTypesPut.addAll(acceptableMediaTypesForPut ?: acceptableMediaTypesForPost)
Expand Down Expand Up @@ -304,7 +322,7 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
called: suspend PipelineContext<Unit, ApplicationCall>.(Layer1Context<TRequestContext, TResponseContext>) -> TUpdateRequest
) {
// add to allowed methods
allowedMethods.add("PATCH")
allowedMethods.add(HttpMethod.Patch)

// add supported RDF content types to accepted PATCH types
acceptedTypesPatch.addAll(acceptableMediaTypesForPatch)
Expand Down Expand Up @@ -332,7 +350,7 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
called: ((Layer1Context<TRequestContext, TResponseContext>) -> Unit)?=null
) {
// add to allowed methods
allowedMethods.add("DELETE")
allowedMethods.add(HttpMethod.Delete)

// define handler
route.delete {
Expand All @@ -350,4 +368,51 @@ abstract class GenericProtocolRoute<TRequestContext: GenericRequest>(
// OPTIONS
@Deprecated("Don't implement OPTIONS. Handled by wrapper")
fun options() {}

// adds "Method not allowed" handling for all other routes
fun otherwiseNotAllowed(
resourcesLabel: String?=null
) {
// HEAD
if(!allowedMethods.contains(HttpMethod.Head)) {
route.head {
checkAllowed(call, resourcesLabel)
}
}

// GET
if(!allowedMethods.contains(HttpMethod.Get)) {
route.get {
checkAllowed(call, resourcesLabel)
}
}

// POST
if(!allowedMethods.contains(HttpMethod.Post)) {
route.post {
checkAllowed(call, resourcesLabel)
}
}

// PUT
if(!allowedMethods.contains(HttpMethod.Put)) {
route.put {
checkAllowed(call, resourcesLabel)
}
}

// PATCH
if(!allowedMethods.contains(HttpMethod.Patch)) {
route.patch {
checkAllowed(call, resourcesLabel)
}
}

// DELETE
if(!allowedMethods.contains(HttpMethod.Delete)) {
route.delete {
checkAllowed(call, resourcesLabel)
}
}
}
}
79 changes: 79 additions & 0 deletions src/test/kotlin/org/openmbee/flexo/mms/ModelLoad.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.openmbee.flexo.mms

import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldStartWith
import io.ktor.http.*
import io.ktor.server.testing.*
import org.openmbee.flexo.mms.util.*

class ModelLoad : ModelAny() {
Expand Down Expand Up @@ -85,5 +88,81 @@ class ModelLoad : ModelAny() {
}
}
}

"head branch graph" {
commitModel(masterBranchPath, insertAliceRex)

withTest {
httpHead("$masterBranchPath/graph") {}.apply {
response shouldHaveStatus HttpStatusCode.OK
// response.content shouldBe null
}
}
}

"get branch graph" {
commitModel(masterBranchPath, insertAliceRex)

withTest {
httpGet("$masterBranchPath/graph") {}.apply {
response shouldHaveStatus HttpStatusCode.OK

response.exclusivelyHasTriples {
subjectTerse(":Alice") {
ignoreAll()
}

subjectTerse(":Rex") {
ignoreAll()
}
}
}
}
}

"head lock graph" {
commitModel(masterBranchPath, insertAliceRex)
createLock(demoRepoPath, masterBranchPath, demoLockId)

withTest {
httpHead("$demoLockPath/graph") {}.apply {
response shouldHaveStatus HttpStatusCode.OK
response.content shouldBe null
}
}
}

"get lock graph" {
commitModel(masterBranchPath, insertAliceRex)
createLock(demoRepoPath, masterBranchPath, demoLockId)

withTest {
httpGet("$demoLockPath/graph") {}.apply {
response shouldHaveStatus HttpStatusCode.OK

response.exclusivelyHasTriples {
subjectTerse(":Alice") {
ignoreAll()
}

subjectTerse(":Rex") {
ignoreAll()
}
}
}
}
}

"lock graph rejects other methods" {
commitModel(masterBranchPath, insertAliceRex)
createLock(demoRepoPath, masterBranchPath, demoLockId)

withTest {
onlyAllowsMethods("$demoLockPath/graph", setOf(
HttpMethod.Head,
HttpMethod.Get,
))
}
}
}
}
Loading