Skip to content

Commit

Permalink
Refactor: Support BoolConditionMatcher (#61)
Browse files Browse the repository at this point in the history
* Refactor: Support `BoolConditionMatcher`

* update README.md
Ahoo-Wang authored Jan 7, 2023

Verified

This commit was signed with the committer’s verified signature.
1 parent ba7db16 commit c17b767
Showing 10 changed files with 360 additions and 6 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -159,9 +159,50 @@ RBAC-based And Policy-based Multi-Tenant Reactive Security Framework.
"pattern": "^中国\\|0\\|(上海|广东省)\\|.*"
}
]
},
{
"name": "AllowDeveloperOrIpRange",
"effect": "allow",
"actions": [
{
"type": "all"
}
],
"conditions": [
{
"type": "bool",
"bool": {
"and": [
{
"type": "authenticated"
}
],
"or": [
{
"type": "in",
"part": "context.principal.id",
"in": [
"developerId"
]
},
{
"type": "path",
"part": "request.remoteIp",
"path": {
"caseSensitive": false,
"separator": ".",
"decodeAndParseSegments": false
},
"pattern": "192.168.0.*"
}
]
}
}
]
}
]
}

```

## Thanks
41 changes: 41 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -159,9 +159,50 @@
"pattern": "^中国\\|0\\|(上海|广东省)\\|.*"
}
]
},
{
"name": "AllowDeveloperOrIpRange",
"effect": "allow",
"actions": [
{
"type": "all"
}
],
"conditions": [
{
"type": "bool",
"bool": {
"and": [
{
"type": "authenticated"
}
],
"or": [
{
"type": "in",
"part": "context.principal.id",
"in": [
"developerId"
]
},
{
"type": "path",
"part": "request.remoteIp",
"path": {
"caseSensitive": false,
"separator": ".",
"decodeAndParseSegments": false
},
"pattern": "192.168.0.*"
}
]
}
}
]
}
]
}

```

## 感谢
Original file line number Diff line number Diff line change
@@ -23,9 +23,10 @@ import org.springframework.http.server.PathContainer

class PathActionMatcher(configuration: Configuration) :
AbstractActionMatcher(PathActionMatcherFactory.TYPE, configuration) {
private val pathPattern = configuration.asPathPatternParser().parse(configuration.getMatcherPattern())
private val patternParser = configuration.asPathPatternParser()
private val pathPattern = patternParser.parse(configuration.getMatcherPattern())
override fun internalMatch(request: Request, securityContext: SecurityContext): Boolean {
PathContainer.parsePath(request.path).let {
PathContainer.parsePath(request.path, patternParser.pathOptions).let {
return pathPattern.matches(it)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright [2021-present] [ahoo wang <[email protected]> (https://github.com/Ahoo-Wang)].
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package me.ahoo.cosec.policy.condition

import me.ahoo.cosec.api.configuration.Configuration
import me.ahoo.cosec.api.configuration.asPojo
import me.ahoo.cosec.api.context.SecurityContext
import me.ahoo.cosec.api.context.request.Request
import me.ahoo.cosec.api.policy.ConditionMatcher

const val BOOL_CONDITION_MATCHER_AND_KEY = "and"
const val BOOL_CONDITION_MATCHER_OR_KEY = "or"

class BoolConditionMatcher(configuration: Configuration) : AbstractConditionMatcher(configuration) {
override val type: String
get() = BoolConditionMatcherFactory.TYPE
val and: List<ConditionMatcher> =
configuration.get(BoolConditionMatcherFactory.TYPE)
?.get(BOOL_CONDITION_MATCHER_AND_KEY)
?.asList()
?.map { it.asPojo<ConditionMatcher>() }.orEmpty()
val or: List<ConditionMatcher> =
configuration.get(BoolConditionMatcherFactory.TYPE)
?.get(BOOL_CONDITION_MATCHER_OR_KEY)
?.asList()
?.map { it.asPojo<ConditionMatcher>() }.orEmpty()

override fun internalMatch(request: Request, securityContext: SecurityContext): Boolean {
and.any {
!it.match(request, securityContext)
}.let {
if (it) {
return false
}
}
if (or.isEmpty()) {
return true
}
or.any {
it.match(request, securityContext)
}.let {
if (it) {
return true
}
}
return false
}
}

class BoolConditionMatcherFactory : ConditionMatcherFactory {
companion object {
const val TYPE = "bool"
}

override val type: String
get() = TYPE

override fun create(configuration: Configuration): ConditionMatcher {
return BoolConditionMatcher(configuration)
}
}
Original file line number Diff line number Diff line change
@@ -25,10 +25,11 @@ class PathConditionMatcher(configuration: Configuration) :
PartConditionMatcher(PathConditionMatcherFactory.TYPE, configuration) {
override val type: String
get() = PathConditionMatcherFactory.TYPE
private val pathPattern: PathPattern = configuration.asPathPatternParser().parse(configuration.getMatcherPattern())
private val patternParser = configuration.asPathPatternParser()
private val pathPattern: PathPattern = patternParser.parse(configuration.getMatcherPattern())

override fun matchPart(partValue: String): Boolean {
PathContainer.parsePath(partValue).let {
PathContainer.parsePath(partValue, patternParser.pathOptions).let {
return pathPattern.matches(it)
}
}
Original file line number Diff line number Diff line change
@@ -23,4 +23,5 @@ me.ahoo.cosec.policy.condition.part.RegularConditionMatcherFactory
me.ahoo.cosec.policy.condition.part.PathConditionMatcherFactory
me.ahoo.cosec.policy.condition.SpelConditionMatcherFactory
me.ahoo.cosec.policy.condition.OgnlConditionMatcherFactory
me.ahoo.cosec.policy.condition.BoolConditionMatcherFactory

Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright [2021-present] [ahoo wang <[email protected]> (https://github.com/Ahoo-Wang)].
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package me.ahoo.cosec.policy.condition

import io.mockk.every
import io.mockk.mockk
import me.ahoo.cosec.api.context.SecurityContext
import me.ahoo.cosec.api.context.request.Request
import me.ahoo.cosec.api.policy.Policy
import me.ahoo.cosec.configuration.JsonConfiguration
import me.ahoo.cosec.configuration.JsonConfiguration.Companion.asConfiguration
import me.ahoo.cosec.policy.MATCHER_TYPE_KEY
import me.ahoo.cosec.serialization.CoSecJsonSerializer
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
import org.junit.jupiter.api.Test

class BoolConditionMatcherTest {
@Test
fun matchWhenEmpty() {
val conditionMatcher = BoolConditionMatcherFactory().create(JsonConfiguration.EMPTY)
assertThat(conditionMatcher.type, `is`(BoolConditionMatcherFactory.TYPE))
assertThat(conditionMatcher.configuration, `is`(JsonConfiguration.EMPTY))
assertThat(conditionMatcher.match(mockk(), mockk()), `is`(true))
}

@Test
fun matchWhenAndOneAll() {
val conditionMatcher = BoolConditionMatcherFactory().create(
mapOf(
MATCHER_TYPE_KEY to BoolConditionMatcherFactory.TYPE,
BoolConditionMatcherFactory.TYPE to mapOf<String, Any>(
BOOL_CONDITION_MATCHER_AND_KEY to listOf(
mapOf<String, Any>(
MATCHER_TYPE_KEY to AllConditionMatcherFactory.TYPE,
)
)
)
).asConfiguration()
) as BoolConditionMatcher
assertThat(conditionMatcher.type, `is`(BoolConditionMatcherFactory.TYPE))
assertThat(conditionMatcher.and, hasSize(1))
assertThat(conditionMatcher.and.first(), instanceOf(AllConditionMatcher::class.java))
assertThat(conditionMatcher.or, empty())
assertThat(conditionMatcher.match(mockk(), mockk()), `is`(true))
}

@Test
fun matchWhenOrOneAll() {
val conditionMatcher = BoolConditionMatcherFactory().create(
mapOf(
MATCHER_TYPE_KEY to BoolConditionMatcherFactory.TYPE,
BoolConditionMatcherFactory.TYPE to mapOf<String, Any>(
BOOL_CONDITION_MATCHER_OR_KEY to listOf(
mapOf<String, Any>(
MATCHER_TYPE_KEY to AllConditionMatcherFactory.TYPE,
)
)
)
).asConfiguration()
) as BoolConditionMatcher
assertThat(conditionMatcher.type, `is`(BoolConditionMatcherFactory.TYPE))
assertThat(conditionMatcher.or, hasSize(1))
assertThat(conditionMatcher.or.first(), instanceOf(AllConditionMatcher::class.java))
assertThat(conditionMatcher.and, empty())
assertThat(conditionMatcher.match(mockk(), mockk()), `is`(true))
}

@Test
fun matchWhenAndOneAllOrOneAll() {
val conditionMatcher = BoolConditionMatcherFactory().create(
mapOf(
MATCHER_TYPE_KEY to BoolConditionMatcherFactory.TYPE,
BoolConditionMatcherFactory.TYPE to mapOf<String, Any>(
BOOL_CONDITION_MATCHER_AND_KEY to listOf(
mapOf<String, Any>(
MATCHER_TYPE_KEY to AllConditionMatcherFactory.TYPE,
)
),
BOOL_CONDITION_MATCHER_OR_KEY to listOf(
mapOf<String, Any>(
MATCHER_TYPE_KEY to AllConditionMatcherFactory.TYPE,
)
)
)
).asConfiguration()
) as BoolConditionMatcher
assertThat(conditionMatcher.type, `is`(BoolConditionMatcherFactory.TYPE))
assertThat(conditionMatcher.and, hasSize(1))
assertThat(conditionMatcher.and.first(), instanceOf(AllConditionMatcher::class.java))
assertThat(conditionMatcher.or, hasSize(1))
assertThat(conditionMatcher.or.first(), instanceOf(AllConditionMatcher::class.java))
assertThat(conditionMatcher.match(mockk(), mockk()), `is`(true))
}

@Test
fun matchGivenJson() {
val testPolicy = requireNotNull(javaClass.classLoader.getResource("test-policy.json")).let { resource ->
resource.openStream().use {
CoSecJsonSerializer.readValue(it, Policy::class.java)
}
}

val conditionMatcher = testPolicy.statements.first { it.name == "AllowDeveloperOrIpRange" }.conditions.first()
assertThat(conditionMatcher, instanceOf(BoolConditionMatcher::class.java))
val developerId = "developerId"
val matchedContext: SecurityContext = mockk {
every { principal.authenticated() } returns true
every { principal.id } returns developerId
}
val notMatchedContext: SecurityContext = mockk {
every { principal.authenticated() } returns true
every { principal.id } returns "not-developerId"
}
val matchedRequest: Request = mockk {
every { remoteIp } returns "192.168.0.1"
}
val notMatchedRequest: Request = mockk {
every { remoteIp } returns "192.168.1.1"
}

assertThat(conditionMatcher.match(notMatchedRequest, matchedContext), `is`(true))
assertThat(conditionMatcher.match(matchedRequest, notMatchedContext), `is`(true))
assertThat(conditionMatcher.match(notMatchedRequest, notMatchedContext), `is`(false))
}
}
40 changes: 40 additions & 0 deletions cosec-core/src/test/resources/test-policy.json
Original file line number Diff line number Diff line change
@@ -107,6 +107,46 @@
"pattern": "^中国\\|0\\|(上海|广东省)\\|.*"
}
]
},
{
"name": "AllowDeveloperOrIpRange",
"effect": "allow",
"actions": [
{
"type": "all"
}
],
"conditions": [
{
"type": "bool",
"bool": {
"and": [
{
"type": "authenticated"
}
],
"or": [
{
"type": "in",
"part": "context.principal.id",
"in": [
"developerId"
]
},
{
"type": "path",
"part": "request.remoteIp",
"path": {
"caseSensitive": false,
"separator": ".",
"decodeAndParseSegments": false
},
"pattern": "192.168.0.*"
}
]
}
}
]
}
]
}
Loading

0 comments on commit c17b767

Please sign in to comment.