diff --git a/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt b/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt index c84de31..718485b 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt @@ -2,10 +2,7 @@ package org.openmbee.flexo.mms import io.ktor.server.request.* import io.ktor.server.response.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.* import org.apache.jena.graph.Triple import org.apache.jena.query.QueryFactory import org.apache.jena.sparql.core.Var @@ -19,22 +16,7 @@ import org.openmbee.flexo.mms.server.SparqlQueryRequest class QuerySyntaxException(parse: Exception): Exception(parse.stackTraceToString()) -/** - * Checks that all necessary conditions are met (i.e., branch state, access control, etc.) before parsing and transforming - * a user's SPARQL query by adding patterns that constrain what graph(s) it will select from. It then submits the - * transformed user query, handling any condition failures, and returns the results to the client. - */ -suspend fun AnyLayer1Context.processAndSubmitUserQuery(queryRequest: SparqlQueryRequest, refIri: String, conditions: ConditionsGroup, addPrefix: Boolean=false, baseIri: String?=null) { - // for certain sparql, point user query at a predetermined graph - var targetGraphIri = when(refIri) { - prefixes["mor"] -> { - "${prefixes["mor-graph"]}Metadata" - } - else -> { - null - } - } - +suspend fun AnyLayer1Context.checkModelQueryConditions(targetGraphIri: String?, refIri: String, conditions: ConditionsGroup): JsonArray { // prepare a query to check required conditions and select the appropriate target graph if necessary val serviceQuery = """ select ?targetGraph ?satisfied where { @@ -111,12 +93,32 @@ suspend fun AnyLayer1Context.processAndSubmitUserQuery(queryRequest: SparqlQuery // parse check response and route to appropriate handler parseConstructResponse(checkResponseText) { - conditions.handle(model, this@processAndSubmitUserQuery) + conditions.handle(model, this@checkModelQueryConditions) } // handler did not terminate connection throw ServerBugException("A required condition was not satisfied, but the condition did not handle the exception") } + return bindings +} + +/** + * Checks that all necessary conditions are met (i.e., branch state, access control, etc.) before parsing and transforming + * a user's SPARQL query by adding patterns that constrain what graph(s) it will select from. It then submits the + * transformed user query, handling any condition failures, and returns the results to the client. + */ +suspend fun AnyLayer1Context.processAndSubmitUserQuery(queryRequest: SparqlQueryRequest, refIri: String, conditions: ConditionsGroup, addPrefix: Boolean=false, baseIri: String?=null) { + // for certain sparql, point user query at a predetermined graph + var targetGraphIri = when(refIri) { + prefixes["mor"] -> { + "${prefixes["mor-graph"]}Metadata" + } + else -> { + null + } + } + + val bindings = checkModelQueryConditions(targetGraphIri, refIri, conditions) // extract the target graph iri from query results if(targetGraphIri == null) { diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt index f2caa2b..60eb03f 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt @@ -1,12 +1,12 @@ 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 +import org.openmbee.flexo.mms.server.SparqlQueryRequest enum class RefType { BRANCH, @@ -23,67 +23,33 @@ suspend fun GspLayer1Context.readModel(refType: RefType) { } } - val authorizedIri = "<${MMS_URNS.SUBJECT.auth}:${transactionId}>" - val constructString = buildSparqlQuery { - construct { - raw(""" - $authorizedIri <${MMS_URNS.PREDICATE.policy}> ?__mms_authMethod . - - ?s ?p ?o - """) - } - where { - 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 { - ${when(refType) { - RefType.BRANCH -> "morb:" - RefType.LOCK -> "morl:" - }} mms:commit/^mms:commit ?ref . - - ?ref mms:snapshot ?modelSnapshot . - - ?modelSnapshot a mms:Model ; - mms:graph ?modelGraph . - } - - graph ?modelGraph { - ?s ?p ?o . - } - """) - } - } - - val constructResponseText = executeSparqlConstructOrDescribe(constructString) { - acceptReplicaLag = true - - prefixes(prefixes) - } - - if(!constructResponseText.contains(authorizedIri)) { - log("Rejecting unauthorized request with 404\n${constructResponseText}") - - if(call.application.glomarResponse) { - throw Http404Exception(call.request.path()) - } - else { - throw Http403Exception(this, call.request.path()) - } - } // HEAD method - else if(call.request.httpMethod == HttpMethod.Head) { + if (call.request.httpMethod == HttpMethod.Head) { + when(refType) { + RefType.BRANCH -> checkModelQueryConditions(null, prefixes["morb"]!!, BRANCH_QUERY_CONDITIONS.append { + assertPreconditions(this) + }) + RefType.LOCK -> checkModelQueryConditions(null, prefixes["morl"]!!, LOCK_QUERY_CONDITIONS.append { + assertPreconditions(this) + }) + } 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 construct = """ + construct { ?s ?p ?o } WHERE { ?s ?p ?o } + """.trimIndent() + val requestContext = SparqlQueryRequest(call, construct, setOf(), setOf()) + when(refType) { + RefType.BRANCH -> processAndSubmitUserQuery(requestContext, prefixes["morb"]!!, BRANCH_QUERY_CONDITIONS.append { + assertPreconditions(this) + }) + RefType.LOCK -> processAndSubmitUserQuery(requestContext, prefixes["morl"]!!, LOCK_QUERY_CONDITIONS.append { + assertPreconditions(this) + }) + } - call.respondText(modelText, contentType=RdfContentTypes.Turtle) } } diff --git a/src/test/kotlin/org/openmbee/flexo/mms/ModelLoad.kt b/src/test/kotlin/org/openmbee/flexo/mms/ModelLoad.kt index 97e665a..653a596 100644 --- a/src/test/kotlin/org/openmbee/flexo/mms/ModelLoad.kt +++ b/src/test/kotlin/org/openmbee/flexo/mms/ModelLoad.kt @@ -89,6 +89,18 @@ class ModelLoad : ModelAny() { } } + "lock graph rejects other methods" { + commitModel(masterBranchPath, insertAliceRex) + createLock(demoRepoPath, masterBranchPath, demoLockId) + + withTest { + onlyAllowsMethods("$demoLockPath/graph", setOf( + HttpMethod.Head, + HttpMethod.Get, + )) + } + } + "head branch graph" { commitModel(masterBranchPath, insertAliceRex) @@ -152,17 +164,5 @@ class ModelLoad : ModelAny() { } } } - - "lock graph rejects other methods" { - commitModel(masterBranchPath, insertAliceRex) - createLock(demoRepoPath, masterBranchPath, demoLockId) - - withTest { - onlyAllowsMethods("$demoLockPath/graph", setOf( - HttpMethod.Head, - HttpMethod.Get, - )) - } - } } } diff --git a/src/test/kotlin/org/openmbee/flexo/mms/ModelRead.kt b/src/test/kotlin/org/openmbee/flexo/mms/ModelRead.kt new file mode 100644 index 0000000..6df6cca --- /dev/null +++ b/src/test/kotlin/org/openmbee/flexo/mms/ModelRead.kt @@ -0,0 +1,86 @@ +import io.kotest.matchers.shouldBe +import io.ktor.http.* +import org.openmbee.flexo.mms.ModelAny +import org.openmbee.flexo.mms.util.* + +class ModelRead : ModelAny() { + init { + listOf( + "head", + "get" + ).forEach { method -> + "$method non-existent model graph" { + withTest { + httpRequest(HttpMethod(method.uppercase()), "$demoLockPath/graph") { + }.apply { + response shouldHaveStatus HttpStatusCode.NotFound + } + } + } + } + + "head branch graph" { + loadModel(masterBranchPath, loadAliceRex) + + withTest { + httpHead("$masterBranchPath/graph") {}.apply { + response shouldHaveStatus HttpStatusCode.OK +// response.content shouldBe null + } + } + } + + "get branch graph" { + loadModel(masterBranchPath, loadAliceRex) + + withTest { + httpGet("$masterBranchPath/graph") {}.apply { + response shouldHaveStatus HttpStatusCode.OK + + response.exclusivelyHasTriples { + subjectTerse(":Alice") { + ignoreAll() + } + + subjectTerse(":Rex") { + ignoreAll() + } + } + } + } + } + + "head lock graph" { + loadModel(masterBranchPath, loadAliceRex) + createLock(demoRepoPath, masterBranchPath, demoLockId) + + withTest { + httpHead("$demoLockPath/graph") {}.apply { + response shouldHaveStatus HttpStatusCode.OK + response.content shouldBe null + } + } + } + + "get lock graph" { + loadModel(masterBranchPath, loadAliceRex) + createLock(demoRepoPath, masterBranchPath, demoLockId) + + withTest { + httpGet("$demoLockPath/graph") {}.apply { + response shouldHaveStatus HttpStatusCode.OK + + response.exclusivelyHasTriples { + subjectTerse(":Alice") { + ignoreAll() + } + + subjectTerse(":Rex") { + ignoreAll() + } + } + } + } + } + } +}