Skip to content

Commit 71b0516

Browse files
abstraktorslisson
authored andcommitted
feat(authorization): allow wildcards in permission grants
Instead of specifying a resource explicitly, the wildcard character '*' can be used to reference all resources of the same type. For example, you can grant write access to all branches without granting write access on the repository itself.
1 parent 7d2c86e commit 71b0516

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

authorization/src/main/kotlin/org/modelix/authorization/permissions/PermissionEvaluator.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,21 @@ class PermissionEvaluator(val schemaInstance: SchemaInstance) {
4848
}
4949

5050
fun hasPermission(permissionInstanceRef: PermissionInstanceReference): Boolean {
51+
require(!permissionInstanceRef.containsWildcards()) {
52+
// An attacker could try to create a resource with the name '*' to get an unintended wildcard grant.
53+
"A permission containing wildcards is illegal: $permissionInstanceRef"
54+
}
55+
for (withWildcards in sequenceOf(permissionInstanceRef) + permissionInstanceRef.getWildcardMutations()) {
56+
if (checkPermission(withWildcards)) return true
57+
}
58+
return false
59+
}
60+
61+
private fun checkPermission(permissionInstanceRef: PermissionInstanceReference): Boolean {
5162
if (allGrantedPermissions.contains(permissionInstanceRef)) return true
5263

5364
val permissionInstance = schemaInstance.instantiatePermission(permissionInstanceRef)
54-
val indirectlyGranted = permissionInstance.includedIn.any { hasPermission(it.ref) }
65+
val indirectlyGranted = permissionInstance.includedIn.any { checkPermission(it.ref) }
5566
if (indirectlyGranted) allGrantedPermissions.add(permissionInstanceRef)
5667
return indirectlyGranted
5768
}

authorization/src/main/kotlin/org/modelix/authorization/permissions/SchemaInstance.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,31 @@ data class ResourceInstanceReference(
156156
fun toPermissionParts(): PermissionParts {
157157
return (parent?.toPermissionParts() ?: PermissionParts()) + name + parameterValues
158158
}
159+
160+
fun getWildcardMutations(): Sequence<ResourceInstanceReference> {
161+
val mutateSelf = parameterValues.size == 1 && parameterValues.single() != WILDCARD
162+
val parentMutations = parent?.getWildcardMutations() ?: emptySequence()
163+
return if (mutateSelf) {
164+
sequenceOf(parent).map {
165+
ResourceInstanceReference(name, listOf(WILDCARD), it)
166+
} + parentMutations.flatMap {
167+
sequenceOf(
168+
ResourceInstanceReference(name, parameterValues, it),
169+
ResourceInstanceReference(name, listOf(WILDCARD), it),
170+
)
171+
}
172+
} else {
173+
parentMutations.map { ResourceInstanceReference(name, parameterValues, it) }
174+
}
175+
}
176+
177+
fun containsWildcards(): Boolean {
178+
return parameterValues.contains(WILDCARD) || parent != null && parent.containsWildcards()
179+
}
180+
181+
companion object {
182+
const val WILDCARD = "*"
183+
}
159184
}
160185

161186
data class PermissionInstanceReference(val permissionName: String, val resource: ResourceInstanceReference) {
@@ -172,4 +197,9 @@ data class PermissionInstanceReference(val permissionName: String, val resource:
172197
false
173198
}
174199
}
200+
fun getWildcardMutations(): Sequence<PermissionInstanceReference> {
201+
return resource.getWildcardMutations().map { PermissionInstanceReference(permissionName, it) }
202+
}
203+
204+
fun containsWildcards() = resource.containsWildcards()
175205
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package permissions
2+
3+
import org.modelix.model.server.ModelServerPermissionSchema
4+
import kotlin.test.Test
5+
import kotlin.test.assertTrue
6+
7+
class ImpliedPermissionsTest : PermissionTestBase(
8+
listOf(
9+
ModelServerPermissionSchema.repository("myFirstRepo").rewrite,
10+
),
11+
) {
12+
13+
@Test
14+
fun `can delete any branch`() {
15+
assertTrue(evaluator.hasPermission("repository/myFirstRepo/branch/just-any/delete"))
16+
}
17+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package permissions
2+
3+
import org.junit.jupiter.api.Disabled
4+
import org.modelix.authorization.permissions.PermissionParts
5+
import kotlin.test.Test
6+
import kotlin.test.assertFalse
7+
import kotlin.test.assertTrue
8+
9+
class WildcardBranchPermissionsTest : PermissionTestBase(
10+
listOf(
11+
PermissionParts.fromString("repository/myFirstRepo/branch/explicitly-deletable/delete"),
12+
PermissionParts.fromString("repository/myFirstRepo/branch/*/write"),
13+
PermissionParts.fromString("repository/myFirstRepo/branch/*/delete"),
14+
),
15+
) {
16+
17+
@Test
18+
fun `can push to branch matching wildcard`() {
19+
assertTrue(evaluator.hasPermission("repository/myFirstRepo/branch/user-named%2Ffeature-123/push"))
20+
}
21+
22+
@Test
23+
fun `cannot force push since that was granted nowhere`() {
24+
assertFalse(evaluator.hasPermission("repository/myFirstRepo/branch/user-named%2Ffeature-123/force-push"))
25+
}
26+
27+
@Disabled
28+
@Test
29+
fun `cannot push to branch not matching wildcard`() {
30+
assertFalse(evaluator.hasPermission("repository/myFirstRepo/branch/not-allowed/push"))
31+
}
32+
33+
@Test
34+
fun `can delete explicitly deletable branch`() {
35+
assertTrue(evaluator.hasPermission("repository/myFirstRepo/branch/explicitly-deletable/delete"))
36+
}
37+
38+
@Test
39+
fun `can delete branch matching wildcard`() {
40+
assertTrue(evaluator.hasPermission("repository/myFirstRepo/branch/user-named%2Ffeature-123/delete"))
41+
assertTrue(evaluator.hasPermission("repository/myFirstRepo/branch/user-named%2Fbugfix-456/delete"))
42+
assertTrue(evaluator.hasPermission("repository/myFirstRepo/branch/user-named%2Fsubdir%2Ffeature/delete"))
43+
assertTrue(evaluator.hasPermission("repository/myFirstRepo/branch/user-named%2F/delete"))
44+
}
45+
46+
@Disabled
47+
@Test
48+
fun `cannot delete branch not matching wildcard`() {
49+
assertFalse(evaluator.hasPermission("repository/myFirstRepo/branch/non-deletable-branch/delete"))
50+
assertFalse(evaluator.hasPermission("repository/myFirstRepo/branch/user-named/delete"))
51+
}
52+
}

0 commit comments

Comments
 (0)