Skip to content

Instantly share code, notes, and snippets.

@bseib
Last active May 22, 2022 20:20
Show Gist options
  • Save bseib/fbc3121ac91328162478ce6ee431b8e7 to your computer and use it in GitHub Desktop.
Save bseib/fbc3121ac91328162478ce6ee431b8e7 to your computer and use it in GitHub Desktop.
Succinct and/or composable/nestable role based access control
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>()
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)
}
}
}
@bseib
Copy link
Author

bseib commented May 22, 2022

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 your accessRequirement variable and it will collapse all the redundant and no-op rules into the simplest rule possible. Then call toString() to see the human readable format.

This unit test is a good demonstration of this:

            val rule1 = allOfAccessRequirements(
                requireRole(Role.AAA),
                alwaysAllow()
            )
            assertEquals("AllOf[AAA, alwaysAllow]", rule1.toString())
            assertEquals("require[AAA]", rule1.reduce().toString())

Have a look at the other unit tests for more demonstrations of reduce().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment