Last active
May 22, 2022 20:20
-
-
Save bseib/fbc3121ac91328162478ce6ee431b8e7 to your computer and use it in GitHub Desktop.
Succinct and/or composable/nestable role based access control
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sealed class AccessRequirement<R> { | |
fun isSatisfiedByRoles(callerRoles: Set<R>): Boolean { | |
return when (this) { | |
is AlwaysAllow -> true | |
is NeverAllow -> false | |
is RequireRole -> callerRoles.contains(role) | |
is AnyOfRoles -> (callerRoles intersect anyOfRoles).isNotEmpty() | |
is AllOfRoles -> callerRoles.containsAll(allOfRoles) | |
is AnyOfAccessRequirements -> anyOf.any { it.isSatisfiedByRoles(callerRoles) } | |
is AllOfAccessRequirements -> allOf.all { it.isSatisfiedByRoles(callerRoles) } | |
} | |
} | |
abstract fun reduceSelf(): AccessRequirement<R> | |
fun reduce(previousState: String? = null): AccessRequirement<R> { | |
return when (this) { | |
is AlwaysAllow, is NeverAllow, is RequireRole -> this | |
is AnyOfRoles, is AllOfRoles, is AnyOfAccessRequirements, is AllOfAccessRequirements -> { | |
val state = this.toString() | |
if (state.equals(previousState)) | |
this | |
else | |
reduceSelf().reduce(state) | |
} | |
} | |
} | |
} | |
fun <R> stringifyAccessRequirementSet(set: Set<AccessRequirement<R>>): String { | |
val renderElement = { rule: AccessRequirement<R> -> | |
when (rule) { | |
is RequireRole -> rule.role.toString() | |
else -> rule.toString() | |
} | |
} | |
return set.joinToString(", ", "[", "]", transform = renderElement) | |
} | |
data class AnyOfAccessRequirements<R>( | |
val anyOf: Set<AccessRequirement<R>>, | |
) : AccessRequirement<R>() { | |
override fun toString() = if (anyOf.isEmpty()) "neverAllow" else "AnyOf${stringifyAccessRequirementSet(anyOf)}" | |
override fun reduceSelf(): AccessRequirement<R> = when (anyOf.size) { | |
0 -> NeverAllow() | |
1 -> anyOf.first() | |
else -> { | |
// remove neverAllows -- they do not alter the evaluation | |
val requirements = anyOf.map { it.reduce() }.toSet() subtract setOf(NeverAllow()) | |
if (requirements.isEmpty()) | |
NeverAllow() | |
else if (requirements.contains(AlwaysAllow())) | |
AlwaysAllow() | |
else if (requirements.all { it is RequireRole || it is AnyOfRoles || it is AnyOfAccessRequirements }) | |
if (requirements.filterIsInstance<AnyOfAccessRequirements<R>>().size > 0) | |
AnyOfAccessRequirements(requirements | |
.flatMap { | |
when (it) { | |
is RequireRole -> listOf(it) | |
is AnyOfRoles -> it.anyOfRoles.map { role -> RequireRole(role) } | |
is AnyOfAccessRequirements -> it.anyOf | |
else -> throw RuntimeException("Unreachable") | |
} | |
} | |
.toSet() | |
) | |
else | |
AnyOfRoles(requirements | |
.flatMap { | |
when (it) { | |
is RequireRole -> listOf(it.role) | |
is AnyOfRoles -> it.anyOfRoles.toList() | |
else -> throw RuntimeException("Unreachable") | |
} | |
} | |
.toSet() | |
) | |
else | |
AnyOfAccessRequirements(requirements) | |
} | |
} | |
} | |
data class AllOfAccessRequirements<R>( | |
val allOf: Set<AccessRequirement<R>>, | |
) : AccessRequirement<R>() { | |
override fun toString() = if (allOf.isEmpty()) "alwaysAllow" else "AllOf${stringifyAccessRequirementSet(allOf)}" | |
override fun reduceSelf(): AccessRequirement<R> = when (allOf.size) { | |
0 -> AlwaysAllow() | |
1 -> allOf.first() | |
else -> { | |
// remove alwaysAllows -- they do not alter the evaluation | |
val requirements = allOf.map { it.reduce() }.toSet() subtract setOf(AlwaysAllow()) | |
if (requirements.isEmpty()) | |
AlwaysAllow() | |
else if (requirements.contains(NeverAllow())) | |
NeverAllow() | |
else if (requirements.all { it is RequireRole || it is AllOfRoles || it is AllOfAccessRequirements }) | |
if (requirements.filterIsInstance<AllOfAccessRequirements<R>>().size > 0) | |
AllOfAccessRequirements(requirements | |
.flatMap { | |
when (it) { | |
is RequireRole -> listOf(it) | |
is AllOfRoles -> it.allOfRoles.map { role -> RequireRole(role) } | |
is AllOfAccessRequirements -> it.allOf | |
else -> throw RuntimeException("Unreachable") | |
} | |
} | |
.toSet() | |
) | |
else | |
AllOfRoles(requirements | |
.flatMap { | |
when (it) { | |
is RequireRole -> listOf(it.role) | |
is AllOfRoles -> it.allOfRoles.toList() | |
else -> throw RuntimeException("Unreachable") | |
} | |
} | |
.toSet() | |
) | |
else | |
AllOfAccessRequirements(requirements) | |
} | |
} | |
} | |
data class AnyOfRoles<R>( | |
val anyOfRoles: Set<R>, | |
) : AccessRequirement<R>() { | |
override fun toString() = if (anyOfRoles.isEmpty()) "neverAllow" else "anyOf${anyOfRoles}" | |
override fun reduceSelf(): AccessRequirement<R> = when (anyOfRoles.size) { | |
0 -> NeverAllow() | |
1 -> RequireRole(anyOfRoles.first()) | |
else -> this | |
} | |
} | |
data class AllOfRoles<R>( | |
val allOfRoles: Set<R>, | |
) : AccessRequirement<R>() { | |
override fun toString() = if (allOfRoles.isEmpty()) "alwaysAllow" else "allOf${allOfRoles}" | |
override fun reduceSelf(): AccessRequirement<R> = when (allOfRoles.size) { | |
0 -> AlwaysAllow() | |
1 -> RequireRole(allOfRoles.first()) | |
else -> this | |
} | |
} | |
data class RequireRole<R>( | |
val role: R, | |
) : AccessRequirement<R>() { | |
override fun toString() = "require[${role.toString()}]" | |
override fun reduceSelf(): AccessRequirement<R> = this | |
} | |
data class AlwaysAllow<R>( | |
val allOfRoles: Set<R> = setOf(), | |
) : AccessRequirement<R>() { | |
override fun toString() = "alwaysAllow" | |
override fun reduceSelf(): AccessRequirement<R> = this | |
} | |
data class NeverAllow<R>( | |
val anyOfRoles: Set<R> = setOf(), | |
) : AccessRequirement<R>() { | |
override fun toString() = "neverAllow" | |
override fun reduceSelf(): AccessRequirement<R> = this | |
} | |
fun <R> anyOfAccessRequirements(vararg reqs: AccessRequirement<R>) = AnyOfAccessRequirements(reqs.toSet()) | |
fun <R> allOfAccessRequirements(vararg reqs: AccessRequirement<R>) = AllOfAccessRequirements(reqs.toSet()) | |
fun <R> requireAnyOfRoles(vararg roles: R) = AnyOfRoles(roles.toSet()) | |
fun <R> requireAllOfRoles(vararg roles: R) = AllOfRoles(roles.toSet()) | |
fun <R> requireRole(role: R) = RequireRole(role) | |
fun <R> neverAllow() = NeverAllow<R>() | |
fun <R> alwaysAllow() = AlwaysAllow<R>() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.apache.commons.math3.util.CombinatoricsUtils | |
import org.junit.jupiter.api.Nested | |
import org.junit.jupiter.api.Test | |
import kotlin.test.assertEquals | |
import kotlin.test.assertTrue | |
class TestAccessRequirement { | |
enum class Role { | |
AAA, | |
BBB, | |
CCC, | |
DDD, | |
EEE | |
} | |
val roleCombinations = mutableListOf<List<Role>>().apply { | |
val allRoles = Role.values().toList() | |
val max = allRoles.size | |
for (take in 1..max) { | |
CombinatoricsUtils | |
.combinationsIterator(max, take) | |
.asSequence() | |
.forEach { combination -> | |
add(combination.toList().map { allRoles[it] }) | |
} | |
} | |
} | |
private fun assertReductionIsEquivalent(rule: AccessRequirement<Role>) { | |
roleCombinations | |
.map { it.toSet() } | |
.forEach { roles -> | |
assertEquals(rule.isSatisfiedByRoles(roles), rule.reduce().isSatisfiedByRoles(roles)) | |
} | |
} | |
@Nested | |
inner class `basic access requirement satisfaction tests` { | |
@Test | |
fun `satisfies any of roles`() { | |
val rule = requireAnyOfRoles(Role.AAA, Role.CCC) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.DDD))) | |
} | |
@Test | |
fun `satisfies all of roles`() { | |
val rule = requireAllOfRoles(Role.AAA, Role.CCC) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.DDD))) | |
} | |
@Test | |
fun `satisfies compound any rule`() { | |
val rule = anyOfAccessRequirements( | |
requireAllOfRoles(Role.AAA, Role.CCC), | |
requireAnyOfRoles(Role.DDD) | |
) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.DDD))) | |
} | |
@Test | |
fun `satisfies compound all rule`() { | |
val rule = allOfAccessRequirements( | |
requireAllOfRoles(Role.AAA), | |
requireAnyOfRoles(Role.CCC, Role.DDD) | |
) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC, Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC, Role.DDD))) | |
} | |
@Test | |
fun `satisfies compound-compound any all rule`() { | |
val rule = anyOfAccessRequirements( | |
requireAnyOfRoles(Role.BBB), | |
allOfAccessRequirements( | |
requireAllOfRoles(Role.AAA), | |
requireAnyOfRoles(Role.CCC, Role.DDD) | |
) | |
) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC, Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC, Role.DDD))) | |
} | |
@Test | |
fun `denied on empty anyOfRoles`() { | |
val rule = neverAllow<Role>() | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.BBB))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC, Role.DDD))) | |
assertEquals(false, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC, Role.DDD))) | |
} | |
@Test | |
fun `satified on empty allOfRoles`() { | |
val rule = alwaysAllow<Role>() | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.BBB))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.AAA, Role.CCC, Role.DDD))) | |
assertEquals(true, rule.isSatisfiedByRoles(setOf(Role.BBB, Role.CCC, Role.DDD))) | |
} | |
} | |
@Nested | |
inner class `basic rule reduction tests` { | |
@Test | |
fun `reduces simplest rules correctly`() { | |
val sameAsNeverAllow = AnyOfRoles<Role>(setOf()) | |
assertEquals("neverAllow", sameAsNeverAllow.toString()) | |
assertTrue(sameAsNeverAllow.reduce() is NeverAllow) | |
assertEquals("neverAllow", sameAsNeverAllow.reduce().toString()) | |
val sameAsAlwaysAllow = AllOfRoles<Role>(setOf()) | |
assertEquals("alwaysAllow", sameAsAlwaysAllow.toString()) | |
assertTrue(sameAsAlwaysAllow.reduce() is AlwaysAllow) | |
assertEquals("alwaysAllow", sameAsAlwaysAllow.reduce().toString()) | |
val sameAsRequireA = AnyOfRoles<Role>(setOf(Role.AAA)) | |
assertEquals("anyOf[AAA]", sameAsRequireA.toString()) | |
assertTrue(sameAsRequireA.reduce() is RequireRole) | |
assertEquals("require[AAA]", sameAsRequireA.reduce().toString()) | |
val sameAsRequireB = AllOfRoles<Role>(setOf(Role.BBB)) | |
assertEquals("allOf[BBB]", sameAsRequireB.toString()) | |
assertTrue(sameAsRequireB.reduce() is RequireRole) | |
assertEquals("require[BBB]", sameAsRequireB.reduce().toString()) | |
val notReducibleAB = AnyOfRoles<Role>(setOf(Role.AAA, Role.BBB)) | |
assertEquals("anyOf[AAA, BBB]", notReducibleAB.toString()) | |
assertTrue(notReducibleAB.reduce() is AnyOfRoles) | |
assertEquals("anyOf[AAA, BBB]", notReducibleAB.reduce().toString()) | |
val notReducibleCD = AllOfRoles<Role>(setOf(Role.CCC, Role.DDD)) | |
assertEquals("allOf[CCC, DDD]", notReducibleCD.toString()) | |
assertTrue(notReducibleCD.reduce() is AllOfRoles) | |
assertEquals("allOf[CCC, DDD]", notReducibleCD.reduce().toString()) | |
} | |
@Test | |
fun `dedups fundamental elements from set`() { | |
val rule1 = requireAnyOfRoles(Role.AAA, Role.AAA) | |
assertEquals(setOf(Role.AAA), rule1.anyOfRoles) | |
val rule2 = requireAllOfRoles(Role.BBB, Role.BBB) | |
assertEquals(setOf(Role.BBB), rule2.allOfRoles) | |
val rule3 = anyOfAccessRequirements(requireRole(Role.AAA), requireRole(Role.AAA), requireRole(Role.CCC)) | |
assertEquals(setOf(RequireRole(Role.AAA), RequireRole(Role.CCC)), rule3.anyOf) | |
val rule4 = allOfAccessRequirements(requireRole(Role.BBB), requireRole(Role.BBB), requireRole(Role.DDD)) | |
assertEquals(setOf(RequireRole(Role.BBB), RequireRole(Role.DDD)), rule4.allOf) | |
val rule5 = anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.AAA), | |
requireRole(Role.CCC), | |
alwaysAllow(), | |
alwaysAllow(), | |
neverAllow(), | |
neverAllow()) | |
assertEquals(setOf(RequireRole(Role.AAA), RequireRole(Role.CCC), AlwaysAllow(), NeverAllow()), rule5.anyOf) | |
val rule6 = allOfAccessRequirements( | |
requireRole(Role.BBB), | |
requireRole(Role.BBB), | |
requireRole(Role.DDD), | |
alwaysAllow(), | |
alwaysAllow(), | |
neverAllow(), | |
neverAllow()) | |
assertEquals(setOf(RequireRole(Role.BBB), RequireRole(Role.DDD), AlwaysAllow(), NeverAllow()), rule6.allOf) | |
} | |
@Test | |
fun `reduces collapsable anyOf rules correctly when empty`() { | |
val rule1 = AnyOfAccessRequirements<Role>(setOf()) | |
assertEquals("neverAllow", rule1.toString()) | |
assertEquals("neverAllow", rule1.reduce().toString()) | |
} | |
@Test | |
fun `reduces collapsable allOf rules correctly when empty`() { | |
val rule1 = AllOfAccessRequirements<Role>(setOf()) | |
assertEquals("alwaysAllow", rule1.toString()) | |
assertEquals("alwaysAllow", rule1.reduce().toString()) | |
} | |
@Test | |
fun `reduces collapsable anyOf rules correctly when has one element`() { | |
val rule1 = anyOfAccessRequirements(requireRole(Role.AAA)) | |
assertEquals("AnyOf[AAA]", rule1.toString()) | |
assertEquals("require[AAA]", rule1.reduce().toString()) | |
val rule2 = anyOfAccessRequirements<Role>(alwaysAllow()) | |
assertEquals("AnyOf[alwaysAllow]", rule2.toString()) | |
assertEquals("alwaysAllow", rule2.reduce().toString()) | |
val rule3 = anyOfAccessRequirements<Role>(neverAllow()) | |
assertEquals("AnyOf[neverAllow]", rule3.toString()) | |
assertEquals("neverAllow", rule3.reduce().toString()) | |
} | |
@Test | |
fun `reduces collapsable allOf rules correctly when has one element`() { | |
val rule1 = allOfAccessRequirements(requireRole(Role.AAA)) | |
assertEquals("AllOf[AAA]", rule1.toString()) | |
assertEquals("require[AAA]", rule1.reduce().toString()) | |
val rule2 = allOfAccessRequirements<Role>(alwaysAllow()) | |
assertEquals("AllOf[alwaysAllow]", rule2.toString()) | |
assertEquals("alwaysAllow", rule2.reduce().toString()) | |
val rule3 = allOfAccessRequirements<Role>(neverAllow()) | |
assertEquals("AllOf[neverAllow]", rule3.toString()) | |
assertEquals("neverAllow", rule3.reduce().toString()) | |
} | |
@Test | |
fun `reduces multi element anyOf when all sub elements reduce to neverAllow`() { | |
val rule1 = anyOfAccessRequirements( | |
allOfAccessRequirements<Role>(requireRole(Role.AAA), neverAllow()), // reduces to neverAllow | |
allOfAccessRequirements<Role>(requireRole(Role.BBB), neverAllow()) // reduces to neverAllow | |
) | |
assertEquals("AnyOf[AllOf[AAA, neverAllow], AllOf[BBB, neverAllow]]", rule1.toString()) | |
assertEquals("neverAllow", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
@Test | |
fun `reduces multi element anyOf when any one sub element reduces to alwaysAllow`() { | |
val rule1 = anyOfAccessRequirements( | |
anyOfAccessRequirements<Role>(requireRole(Role.AAA), alwaysAllow()), // reduces to alwaysAllow | |
anyOfAccessRequirements<Role>(requireRole(Role.BBB), alwaysAllow()) // reduces to alwaysAllow | |
) | |
assertEquals("AnyOf[AnyOf[AAA, alwaysAllow], AnyOf[BBB, alwaysAllow]]", rule1.toString()) | |
assertEquals("alwaysAllow", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
@Test | |
fun `collapses multi element anyOf when all elements are requireRole`() { | |
val rule1 = anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.BBB), | |
requireRole(Role.CCC), | |
) | |
assertEquals("AnyOf[AAA, BBB, CCC]", rule1.toString()) | |
assertEquals("anyOf[AAA, BBB, CCC]", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
@Test | |
fun `reduces twice when starting with lists of requirements`() { | |
// Here is the example case: | |
//AllOf[rq[AUTHENTICATED], alwaysAllow] -> allOf[AUTHENTICATED] -> rq[AUTHENTICATED] | |
val rule1 = allOfAccessRequirements( | |
requireRole(Role.AAA), | |
alwaysAllow() | |
) | |
assertEquals("AllOf[AAA, alwaysAllow]", rule1.toString()) | |
assertEquals("require[AAA]", rule1.reduce().toString()) | |
} | |
} | |
@Nested | |
inner class `advanced anyOf rule reduction tests` { | |
@Test | |
fun `does not collapse mixed multi element anyOf while always removing neverAllowed`() { | |
val rule1 = anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.AAA), | |
requireRole(Role.CCC), | |
allOfAccessRequirements(requireRole(Role.AAA), requireRole(Role.BBB)), | |
neverAllow()) | |
assertEquals(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.CCC), | |
AllOfAccessRequirements(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.BBB) | |
)), | |
NeverAllow() | |
), rule1.anyOf) | |
assertEquals("AnyOf[AAA, CCC, AllOf[AAA, BBB], neverAllow]", rule1.toString()) | |
assertEquals("AnyOf[AAA, CCC, allOf[AAA, BBB]]", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
@Test | |
fun `collapses nested anyOfRoles`() { | |
val rule1 = anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.AAA), | |
requireRole(Role.CCC), | |
anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.BBB), | |
anyOfAccessRequirements( | |
requireRole(Role.CCC), | |
requireRole(Role.DDD), | |
requireAnyOfRoles( | |
Role.AAA, | |
Role.EEE | |
), | |
), | |
) | |
) | |
assertEquals("AnyOf[AAA, CCC, AnyOf[AAA, BBB, AnyOf[CCC, DDD, anyOf[AAA, EEE]]]]", rule1.toString()) | |
assertEquals("anyOf[AAA, CCC, BBB, DDD, EEE]", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
@Test | |
fun `collapses nested anyOfRequirements`() { | |
val rule1 = anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.AAA), | |
requireRole(Role.CCC), | |
anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.BBB), | |
anyOfAccessRequirements( | |
requireRole(Role.CCC), | |
requireRole(Role.DDD), | |
allOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.EEE) | |
), | |
), | |
), | |
neverAllow()) | |
assertEquals(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.CCC), | |
AnyOfAccessRequirements(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.BBB), | |
AnyOfAccessRequirements(setOf( | |
RequireRole(Role.CCC), | |
RequireRole(Role.DDD), | |
AllOfAccessRequirements(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.EEE) | |
)) | |
)) | |
)), | |
NeverAllow() | |
), rule1.anyOf) | |
assertEquals("AnyOf[AAA, CCC, AnyOf[AAA, BBB, AnyOf[CCC, DDD, AllOf[AAA, EEE]]], neverAllow]", | |
rule1.toString()) | |
assertEquals("AnyOf[AAA, CCC, BBB, DDD, allOf[AAA, EEE]]", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
} | |
@Nested | |
inner class `advanced allOf rule reduction tests` { | |
@Test | |
fun `does not collapse mixed multi element anyOf while always removing alwaysAllowed`() { | |
val rule1 = allOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.AAA), | |
requireRole(Role.CCC), | |
anyOfAccessRequirements(requireRole(Role.AAA), requireRole(Role.BBB)), | |
alwaysAllow()) | |
assertEquals(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.CCC), | |
AnyOfAccessRequirements(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.BBB) | |
)), | |
AlwaysAllow() | |
), rule1.allOf) | |
assertEquals("AllOf[AAA, CCC, AnyOf[AAA, BBB], alwaysAllow]", rule1.toString()) | |
assertEquals("AllOf[AAA, CCC, anyOf[AAA, BBB]]", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
@Test | |
fun `collapses nested allOfRoles`() { | |
val rule1 = allOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.AAA), | |
requireRole(Role.CCC), | |
allOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.BBB), | |
allOfAccessRequirements( | |
requireRole(Role.CCC), | |
requireRole(Role.DDD), | |
requireAllOfRoles( | |
Role.AAA, | |
Role.EEE | |
), | |
), | |
) | |
) | |
assertEquals("AllOf[AAA, CCC, AllOf[AAA, BBB, AllOf[CCC, DDD, allOf[AAA, EEE]]]]", rule1.toString()) | |
assertEquals("allOf[AAA, CCC, BBB, DDD, EEE]", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
@Test | |
fun `collapses nested allOfRequirements`() { | |
val rule1 = allOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.AAA), | |
requireRole(Role.CCC), | |
allOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.BBB), | |
allOfAccessRequirements( | |
requireRole(Role.CCC), | |
requireRole(Role.DDD), | |
anyOfAccessRequirements( | |
requireRole(Role.AAA), | |
requireRole(Role.EEE) | |
), | |
), | |
), | |
alwaysAllow()) | |
assertEquals(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.CCC), | |
AllOfAccessRequirements(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.BBB), | |
AllOfAccessRequirements(setOf( | |
RequireRole(Role.CCC), | |
RequireRole(Role.DDD), | |
AnyOfAccessRequirements(setOf( | |
RequireRole(Role.AAA), | |
RequireRole(Role.EEE) | |
)) | |
)) | |
)), | |
AlwaysAllow() | |
), rule1.allOf) | |
assertEquals("AllOf[AAA, CCC, AllOf[AAA, BBB, AllOf[CCC, DDD, AnyOf[AAA, EEE]]], alwaysAllow]", | |
rule1.toString()) | |
assertEquals("AllOf[AAA, CCC, BBB, DDD, anyOf[AAA, EEE]]", rule1.reduce().toString()) | |
assertReductionIsEquivalent(rule1) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The first revision of this gist is still valid, and more compact.
This revision works exactly the same, but makes the access rules human readable when they are nested. Just call
reduce()
on youraccessRequirement
variable and it will collapse all the redundant and no-op rules into the simplest rule possible. Then calltoString()
to see the human readable format.This unit test is a good demonstration of this:
Have a look at the other unit tests for more demonstrations of
reduce()
.