Skip to content

Commit c755238

Browse files
authored
Merge pull request #9 from JPL-Devin/devin/1776748059-collection-ref-validation
Add ref existence and admin permission validation for collection creation
2 parents c65ed21 + 3973555 commit c755238

6 files changed

Lines changed: 224 additions & 24 deletions

File tree

src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ suspend fun AnyLayer1Context.processAndSubmitUserQuery(queryRequest: SparqlQuery
157157
} else {
158158
graphIris.add(targetGraphIri)
159159
}
160-
160+
if (graphIris.isEmpty()) {
161+
//TODO return empty, otherwise user query will be made against triple store's default graph
162+
}
161163
// parse user query
162164
val userQuery = try {
163165
if(baseIri != null) {

src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/CollectionWrite.kt

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.openmbee.flexo.mms.routes.ldp
22

33
import io.ktor.http.*
4+
import kotlinx.serialization.json.jsonObject
5+
import kotlinx.serialization.json.jsonPrimitive
46
import org.apache.jena.vocabulary.RDF
57
import org.openmbee.flexo.mms.*
68
import org.openmbee.flexo.mms.server.LdpDcLayer1Context
@@ -71,7 +73,108 @@ suspend fun <TResponseContext: LdpMutateResponse> LdpDcLayer1Context<TResponseCo
7173

7274
// retrieve the resolved collects URIs
7375
val collectsUris = call.attributes[COLLECTS_URIS_KEY]
74-
//TODO check user has admin permission on the refs
76+
77+
// Step 1: Verify all refs exist and determine their types
78+
val valuesClause = collectsUris.joinToString(" ") { "<$it>" }
79+
80+
val refExistenceQuery = """
81+
select ?ref ?refType where {
82+
values ?ref { $valuesClause }
83+
#graph m-graph:Cluster { ## having the filter and binding doesn't work and returns nothing?
84+
# ?repo a mms:Repo .
85+
# filter(strstarts(str(?ref), concat(str(?repo), "/")))
86+
#}
87+
# derive the repo metadata graph IRI from the repo IRI
88+
#bind(iri(concat(str(?repo), "/graphs/Metadata")) as ?repoMetaGraph)
89+
graph ?repoMetaGraph {
90+
?ref a ?refType .
91+
filter(?refType in (mms:Branch, mms:Lock, mms:Scratch))
92+
}
93+
}
94+
""".trimIndent()
95+
96+
val existenceResults = executeSparqlSelectOrAsk(refExistenceQuery) {
97+
prefixes(prefixes)
98+
}
99+
val existenceBindings = parseSparqlResultsJsonSelect(existenceResults)
100+
val foundRefTypes = existenceBindings.mapNotNull { binding ->
101+
val ref = binding["ref"]?.jsonObject?.get("value")?.jsonPrimitive?.content
102+
val refType = binding["refType"]?.jsonObject?.get("value")?.jsonPrimitive?.content
103+
if (ref != null && refType != null) ref to refType else null
104+
}.toMap()
105+
106+
for (uri in collectsUris) {
107+
if (uri !in foundRefTypes) {
108+
throw Http404Exception("Ref <$uri> does not exist")
109+
}
110+
}
111+
112+
// Step 2: Verify admin permission on each ref, correlating ref type with required permission
113+
// Derive repo and org scope URIs from the ref URI to avoid the Cluster graph lookup
114+
// (Fuseki does not reliably evaluate filter(strstarts) with VALUES-bound variables)
115+
val clusterUri = prefixes["m"]!!
116+
val refPermissionValues = foundRefTypes.entries.joinToString("\n") { (ref, refType) ->
117+
val requiredPerm = when {
118+
refType.endsWith("Branch") -> "mms-object:Permission.UpdateBranch"
119+
refType.endsWith("Lock") -> "mms-object:Permission.UpdateLock"
120+
refType.endsWith("Scratch") -> "mms-object:Permission.UpdateScratch"
121+
else -> throw Http400Exception("Unknown ref type: $refType")
122+
}
123+
val repoUri = ref.replace(Regex("/(branches|locks|scratches)/.*$"), "")
124+
val orgUri = repoUri.replace(Regex("/repos/.*$"), "")
125+
"( <$ref> $requiredPerm <$repoUri> <$orgUri> <$clusterUri> )"
126+
}
127+
128+
val permissionQuery = """
129+
select ?ref where {
130+
values (?ref ?requiredPerm ?repoScope ?orgScope ?clusterScope) { $refPermissionValues }
131+
132+
graph m-graph:AccessControl.Policies {
133+
?policy a mms:Policy ;
134+
mms:scope ?scope ;
135+
mms:role ?role .
136+
}
137+
138+
{
139+
graph m-graph:AccessControl.Policies {
140+
?policy mms:subject mu: .
141+
}
142+
} union {
143+
graph m-graph:AccessControl.Agents {
144+
?group a mms:Group ;
145+
mms:id ?groupId .
146+
values ?groupId {
147+
# @values groupId
148+
}
149+
}
150+
graph m-graph:AccessControl.Policies {
151+
?policy mms:subject ?group .
152+
}
153+
}
154+
155+
filter(?scope = ?ref || ?scope = ?repoScope || ?scope = ?orgScope || ?scope = ?clusterScope)
156+
157+
graph m-graph:AccessControl.Definitions {
158+
?role a mms:Role ;
159+
mms:permits ?permission .
160+
?permission a mms:Permission ;
161+
mms:implies* ?requiredPerm .
162+
}
163+
}
164+
""".trimIndent()
165+
166+
val permissionResults = executeSparqlSelectOrAsk(permissionQuery) {
167+
prefixes(prefixes)
168+
}
169+
val permissionBindings = parseSparqlResultsJsonSelect(permissionResults)
170+
val permittedRefs = permissionBindings.mapNotNull { it["ref"]?.jsonObject?.get("value")?.jsonPrimitive?.content }.toSet()
171+
172+
for (uri in collectsUris) {
173+
if (uri !in permittedRefs) {
174+
throw Http403Exception(this, "Ref <$uri>")
175+
}
176+
}
177+
75178
// resolve ambiguity
76179
if(intentIsAmbiguous) {
77180
// ask if collection exists

src/test/kotlin/org/openmbee/flexo/mms/CollectionLdpDc.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fun TriplesAsserter.validateCreatedCollectionTriples(
5757

5858
class CollectionLdpDc : CollectionAny() {
5959
init {
60-
"PUT $demoCollectionPath - create valid collection" {
60+
"PUT collection - create valid collection" {
6161
testApplication {
6262
httpPut(demoCollectionPath) {
6363
setTurtleBody(withAllTestPrefixes(validCollectionBody))
@@ -67,7 +67,7 @@ class CollectionLdpDc : CollectionAny() {
6767
}
6868
}
6969

70-
"PUT $demoCollectionPath - replace existing collection" {
70+
"PUT collection - replace existing collection" {
7171
testApplication {
7272
// create initial collection
7373
httpPut(demoCollectionPath) {
@@ -88,15 +88,15 @@ class CollectionLdpDc : CollectionAny() {
8888
}
8989
}
9090

91-
"GET $demoCollectionPath - non-existent" {
91+
"GET collection - non-existent" {
9292
testApplication {
9393
httpGet(demoCollectionPath) {}.apply {
9494
this shouldHaveStatus HttpStatusCode.NotFound
9595
}
9696
}
9797
}
9898

99-
"GET $demoCollectionPath - valid" {
99+
"GET collection - valid" {
100100
testApplication {
101101
httpPut(demoCollectionPath) {
102102
setTurtleBody(withAllTestPrefixes(validCollectionBody))
@@ -111,7 +111,7 @@ class CollectionLdpDc : CollectionAny() {
111111
}
112112
}
113113

114-
"HEAD $demoCollectionPath - valid" {
114+
"HEAD collection - valid" {
115115
testApplication {
116116
httpPut(demoCollectionPath) {
117117
setTurtleBody(withAllTestPrefixes(validCollectionBody))
@@ -123,7 +123,7 @@ class CollectionLdpDc : CollectionAny() {
123123
}
124124
}
125125

126-
"GET $basePathCollections - all collections" {
126+
"GET collections - all collections" {
127127
testApplication {
128128
httpPut(demoCollectionPath) {
129129
setTurtleBody(withAllTestPrefixes(validCollectionBody))
@@ -135,7 +135,7 @@ class CollectionLdpDc : CollectionAny() {
135135
}
136136
}
137137

138-
"PUT $demoCollectionPath - reject missing mms:collects" {
138+
"PUT collection - reject missing mms:collects" {
139139
testApplication {
140140
httpPut(demoCollectionPath) {
141141
setTurtleBody(withAllTestPrefixes("""
@@ -147,7 +147,7 @@ class CollectionLdpDc : CollectionAny() {
147147
}
148148
}
149149

150-
"POST $basePathCollections - create valid collection" {
150+
"POST collections - create valid collection" {
151151
testApplication {
152152
httpPost(basePathCollections) {
153153
header(HttpHeaders.SLUG, demoCollectionId)

src/test/kotlin/org/openmbee/flexo/mms/CollectionQueryTest.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class CollectionQueryTest : CollectionAny() {
1212
override val logger = LoggerFactory.getLogger(CollectionQueryTest::class.java)
1313

1414
init {
15-
"POST $demoCollectionPath/query - non-existent collection returns 404" {
15+
"POST collection query - non-existent collection returns 404" {
1616
testApplication {
1717
httpPost("$demoCollectionPath/query") {
1818
setSparqlQueryBody(queryPersonNames)
@@ -22,7 +22,7 @@ class CollectionQueryTest : CollectionAny() {
2222
}
2323
}
2424

25-
"POST $demoCollectionPath/query - query across branch and lock returns union" {
25+
"POST collection query - query across branch and lock returns union" {
2626
testApplication {
2727
// insert Alice into first repo's master branch
2828
commitModel(masterBranchPath, insertAlice)
@@ -57,7 +57,7 @@ class CollectionQueryTest : CollectionAny() {
5757
}
5858
}
5959

60-
"POST $demoCollectionPath/query - query across branch and scratch returns union" {
60+
"POST collection query - query across branch and scratch returns union" {
6161
testApplication {
6262
// insert Alice into the master branch
6363
commitModel(masterBranchPath, insertAlice)
@@ -91,7 +91,7 @@ class CollectionQueryTest : CollectionAny() {
9191
}
9292
}
9393

94-
"POST $demoCollectionPath/query - query across branch, lock, and scratch returns full union" {
94+
"POST collection query - query across branch, lock, and scratch returns full union" {
9595
testApplication {
9696
// insert Alice into first repo's master branch
9797
commitModel(masterBranchPath, insertAlice)
@@ -134,7 +134,7 @@ class CollectionQueryTest : CollectionAny() {
134134
}
135135
}
136136

137-
"POST $demoCollectionPath/query - ASK query returns true when data exists" {
137+
"POST collection query - ASK query returns true when data exists" {
138138
testApplication {
139139
// insert Alice into the master branch
140140
commitModel(masterBranchPath, insertAlice)
@@ -161,7 +161,7 @@ class CollectionQueryTest : CollectionAny() {
161161
}
162162
}
163163

164-
"POST $demoCollectionPath/query - ASK query returns false when no matching data" {
164+
"POST collection query - ASK query returns false when no matching data" {
165165
testApplication {
166166
// insert Bob (not Alice) into the master branch
167167
commitModel(masterBranchPath, insertBob)
@@ -188,7 +188,7 @@ class CollectionQueryTest : CollectionAny() {
188188
}
189189
}
190190

191-
"POST $demoCollectionPath/query - CONSTRUCT returns union triples" {
191+
"POST collection query - CONSTRUCT returns union triples" {
192192
testApplication {
193193
// insert Alice into master branch
194194
commitModel(masterBranchPath, insertAlice)
@@ -222,7 +222,7 @@ class CollectionQueryTest : CollectionAny() {
222222
}
223223
}
224224

225-
"POST $demoCollectionPath/query - FROM clause rejected with 403" {
225+
"POST collection query - FROM clause rejected with 403" {
226226
testApplication {
227227
// create collection
228228
httpPut(demoCollectionPath) {
@@ -246,7 +246,7 @@ class CollectionQueryTest : CollectionAny() {
246246
}
247247
}
248248

249-
"POST $demoCollectionPath/query - FROM NAMED clause rejected with 403" {
249+
"POST collection query - FROM NAMED clause rejected with 403" {
250250
testApplication {
251251
// create collection
252252
httpPut(demoCollectionPath) {
@@ -272,7 +272,7 @@ class CollectionQueryTest : CollectionAny() {
272272
}
273273
}
274274

275-
"POST $demoCollectionPath/query/inspect - inspect generated query" {
275+
"POST collection query inspect - inspect generated query" {
276276
testApplication {
277277
// insert data and create collection
278278
commitModel(masterBranchPath, insertAlice)
@@ -291,7 +291,7 @@ class CollectionQueryTest : CollectionAny() {
291291
}
292292
}
293293

294-
"GET $demoCollectionPath/graph - get union graph across branch and lock" {
294+
"GET collection graph - get union graph across branch and lock" {
295295
testApplication {
296296
// insert Alice into first repo
297297
commitModel(masterBranchPath, insertAlice)
@@ -324,7 +324,7 @@ class CollectionQueryTest : CollectionAny() {
324324
}
325325
}
326326

327-
"POST $demoCollectionPath/query - single branch collection returns only that branch's data" {
327+
"POST collection query - single branch collection returns only that branch's data" {
328328
testApplication {
329329
// insert Alice into master
330330
commitModel(masterBranchPath, insertAlice)
@@ -350,7 +350,7 @@ class CollectionQueryTest : CollectionAny() {
350350
}
351351
}
352352

353-
"POST $demoCollectionPath/query - lock preserves snapshot at time of locking" {
353+
"POST collection query - lock preserves snapshot at time of locking" {
354354
testApplication {
355355
// insert Alice, then lock
356356
commitModel(masterBranchPath, insertAlice)

0 commit comments

Comments
 (0)