Skip to content

Commit

Permalink
Merge pull request #174 from Open-MBEE/fix/gspRead
Browse files Browse the repository at this point in the history
Update gsp model read to reuse similar logic from model query
  • Loading branch information
dlamoris authored Nov 21, 2024
2 parents db42244 + ec2b87e commit 5deb5cc
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 89 deletions.
44 changes: 23 additions & 21 deletions src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
78 changes: 22 additions & 56 deletions src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,67 +23,33 @@ suspend fun GspLayer1Context<GspReadResponse>.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)
}
}
24 changes: 12 additions & 12 deletions src/test/kotlin/org/openmbee/flexo/mms/ModelLoad.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
))
}
}
}
}
86 changes: 86 additions & 0 deletions src/test/kotlin/org/openmbee/flexo/mms/ModelRead.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
}
}
}

0 comments on commit 5deb5cc

Please sign in to comment.