diff --git a/README.md b/README.md index 531919f9..a67edbfd 100644 --- a/README.md +++ b/README.md @@ -49,95 +49,100 @@ RBAC-based And Policy-based Multi-Tenant Reactive Security Framework. ```json { - "id": "2", - "name": "auth", - "category": "auth", - "description": "", + "id": "id", + "name": "name", + "category": "category", + "description": "description", "type": "global", - "tenantId": "1", + "tenantId": "tenantId", "statements": [ { + "name": "Anonymous", "effect": "allow", "actions": [ { - "type": "all" + "type": "path", + "pattern": "/auth/register" }, { - "type": "none" - }, + "type": "path", + "pattern": "/auth/login" + } + ] + }, + { + "name": "UserScope", + "effect": "allow", + "actions": [ { "type": "path", - "methods": [ - "GET", - "POST", - "PUT", - "DELETE" - ], - "pattern": "/user/{userId}/*" + "pattern": "/user/#{principal.id}/*" } ], "conditions": [ { "type": "authenticated" - }, + } + ] + }, + { + "name": "Developer", + "effect": "allow", + "actions": [ + { + "type": "all" + } + ], + "conditions": [ { "type": "in", "part": "context.principal.id", "in": [ - "userId" + "developerId" ] } ] }, { + "name": "RequestOriginDeny", "effect": "deny", "actions": [ { - "type": "all", - "methods": [ - "GET" - ] - }, - { - "type": "none" - }, - { - "type": "path", - "pattern": ".*" - }, - { - "type": "path", - "pattern": "#{principal.id}.*" - }, + "type": "all" + } + ], + "conditions": [ { "type": "reg", - "pattern": ".*" - }, + "negate": true, + "part": "request.origin", + "pattern": "^(http|https)://github.com" + } + ] + }, + { + "name": "IpBlacklist", + "effect": "deny", + "actions": [ { - "type": "reg", - "pattern": "#{principal.id}.*" + "type": "all" } ], "conditions": [ { - "type": "all" - }, - { - "type": "none" - }, - { - "type": "spel", - "pattern": "context.principal.id=='1'" - }, - { - "type": "ognl", - "pattern": "path == \"auth/login\"" + "type": "path", + "part": "request.remoteIp", + "path": { + "caseSensitive": false, + "separator": ".", + "decodeAndParseSegments": false + }, + "pattern": "192.168.0.*" } ] } ] } - ``` ## Thanks diff --git a/README.zh-CN.md b/README.zh-CN.md index 1a85701e..bdc9c443 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -49,95 +49,100 @@ ```json { - "id": "2", - "name": "auth", - "category": "auth", - "description": "", + "id": "id", + "name": "name", + "category": "category", + "description": "description", "type": "global", - "tenantId": "1", + "tenantId": "tenantId", "statements": [ { + "name": "Anonymous", "effect": "allow", "actions": [ { - "type": "all" + "type": "path", + "pattern": "/auth/register" }, { - "type": "none" - }, + "type": "path", + "pattern": "/auth/login" + } + ] + }, + { + "name": "UserScope", + "effect": "allow", + "actions": [ { "type": "path", - "methods": [ - "GET", - "POST", - "PUT", - "DELETE" - ], - "pattern": "/user/{userId}/*" + "pattern": "/user/#{principal.id}/*" } ], "conditions": [ { "type": "authenticated" - }, + } + ] + }, + { + "name": "Developer", + "effect": "allow", + "actions": [ + { + "type": "all" + } + ], + "conditions": [ { "type": "in", "part": "context.principal.id", "in": [ - "userId" + "developerId" ] } ] }, { + "name": "RequestOriginDeny", "effect": "deny", "actions": [ { - "type": "all", - "methods": [ - "GET" - ] - }, - { - "type": "none" - }, - { - "type": "path", - "pattern": ".*" - }, - { - "type": "path", - "pattern": "#{principal.id}.*" - }, + "type": "all" + } + ], + "conditions": [ { "type": "reg", - "pattern": ".*" - }, + "negate": true, + "part": "request.origin", + "pattern": "^(http|https)://github.com" + } + ] + }, + { + "name": "IpBlacklist", + "effect": "deny", + "actions": [ { - "type": "reg", - "pattern": "#{principal.id}.*" + "type": "all" } ], "conditions": [ { - "type": "all" - }, - { - "type": "none" - }, - { - "type": "spel", - "pattern": "context.principal.id=='1'" - }, - { - "type": "ognl", - "pattern": "path == \"auth/login\"" + "type": "path", + "part": "request.remoteIp", + "path": { + "caseSensitive": false, + "separator": ".", + "decodeAndParseSegments": false + }, + "pattern": "192.168.0.*" } ] } ] } - ``` ## 感谢 diff --git a/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Policy.kt b/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Policy.kt index 5354b3f1..997bd944 100644 --- a/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Policy.kt +++ b/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Policy.kt @@ -24,5 +24,5 @@ interface Policy : Named, Tenant { val category: String val description: String val type: PolicyType - val statements: Set + val statements: List } diff --git a/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Statement.kt b/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Statement.kt index 01fb1e07..4d5f24a1 100644 --- a/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Statement.kt +++ b/cosec-api/src/main/kotlin/me/ahoo/cosec/api/policy/Statement.kt @@ -13,13 +13,15 @@ package me.ahoo.cosec.api.policy +import me.ahoo.cosec.api.Named import me.ahoo.cosec.api.context.SecurityContext import me.ahoo.cosec.api.context.request.Request -interface Statement : PermissionVerifier { +interface Statement : Named, PermissionVerifier { + override val name: String val effect: Effect - val actions: Set - val conditions: Set + val actions: List + val conditions: List override fun verify(request: Request, securityContext: SecurityContext): VerifyResult { conditions.all { diff --git a/cosec-core/src/main/kotlin/me/ahoo/cosec/authorization/SimpleAuthorization.kt b/cosec-core/src/main/kotlin/me/ahoo/cosec/authorization/SimpleAuthorization.kt index 1eaae72d..b4af3e96 100644 --- a/cosec-core/src/main/kotlin/me/ahoo/cosec/authorization/SimpleAuthorization.kt +++ b/cosec-core/src/main/kotlin/me/ahoo/cosec/authorization/SimpleAuthorization.kt @@ -39,11 +39,13 @@ class SimpleAuthorization(private val policyRepository: PolicyRepository) : Auth policies.forEach { policy: Policy -> policy.statements.filter { statement: Statement -> statement.effect == Effect.DENY - }.forEach { statement: Statement -> + }.forEachIndexed { index, statement -> val verifyResult = statement.verify(request, context) if (verifyResult == VerifyResult.EXPLICIT_DENY) { if (log.isDebugEnabled) { - log.debug("Verify [$request] [$context] matched Policy[${policy.id}] - [Explicit Deny].") + log.debug( + "Verify [$request] [$context] matched Policy[${policy.id}] Statement[$index][${statement.name}] - [Explicit Deny]." + ) } return VerifyResult.EXPLICIT_DENY } @@ -53,11 +55,13 @@ class SimpleAuthorization(private val policyRepository: PolicyRepository) : Auth policies.forEach { policy: Policy -> policy.statements.filter { statement: Statement -> statement.effect == Effect.ALLOW - }.forEach { statement: Statement -> + }.forEachIndexed { index, statement -> val verifyResult = statement.verify(request, context) if (verifyResult == VerifyResult.ALLOW) { if (log.isDebugEnabled) { - log.debug("Verify [$request] [$context] matched Policy[${policy.id}] - [Allow].") + log.debug( + "Verify [$request] [$context] matched Policy[${policy.id}] Statement[$index][${statement.name}] - [Allow]." + ) } return VerifyResult.ALLOW } diff --git a/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/PolicyData.kt b/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/PolicyData.kt index fbcd6ff8..cf9b9a9e 100644 --- a/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/PolicyData.kt +++ b/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/PolicyData.kt @@ -17,12 +17,31 @@ import me.ahoo.cosec.api.policy.Policy import me.ahoo.cosec.api.policy.PolicyType import me.ahoo.cosec.api.policy.Statement -data class PolicyData( +class PolicyData( override val id: String, override val category: String, override val name: String, override val description: String, override val type: PolicyType, override val tenantId: String, - override val statements: Set = emptySet() -) : Policy + override val statements: List = listOf() +) : Policy { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PolicyData + + if (id != other.id) return false + if (tenantId != other.tenantId) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + tenantId.hashCode() + return result + } +} diff --git a/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/StatementData.kt b/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/StatementData.kt index ddaf682f..20fd5dfe 100644 --- a/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/StatementData.kt +++ b/cosec-core/src/main/kotlin/me/ahoo/cosec/policy/StatementData.kt @@ -19,7 +19,8 @@ import me.ahoo.cosec.api.policy.Effect import me.ahoo.cosec.api.policy.Statement data class StatementData( + override val name: String = "", override val effect: Effect = Effect.ALLOW, - override val actions: Set = emptySet(), - override val conditions: Set = emptySet() + override val actions: List = listOf(), + override val conditions: List = listOf() ) : Statement diff --git a/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonPolicySerializer.kt b/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonPolicySerializer.kt index dd8e8dba..5d076e61 100644 --- a/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonPolicySerializer.kt +++ b/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonPolicySerializer.kt @@ -56,23 +56,30 @@ object JsonPolicySerializer : StdSerializer(Policy::class.java) { object JsonPolicyDeserializer : StdDeserializer(Policy::class.java) { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Policy { val jsonNode = p.codec.readTree(p) - val statements = jsonNode.has(POLICY_STATEMENTS_KEY).let { hasStatements -> - if (hasStatements) { - jsonNode.get(POLICY_STATEMENTS_KEY).map { - it.traverse(p.codec).readValueAs(Statement::class.java) - }.toSet() - } else { - emptySet() - } - } + + val statements = jsonNode.get(POLICY_STATEMENTS_KEY)?.map { + it.traverse(p.codec).readValueAs(Statement::class.java) + }.orEmpty() return PolicyData( - id = jsonNode.get(POLICY_ID_KEY).asText(), - name = jsonNode.get(POLICY_NAME_KEY).asText(), - category = jsonNode.get(POLICY_CATEGORY_KEY).asText(), - description = jsonNode.get(POLICY_DESCRIPTION_KEY).asText(), - type = jsonNode.get(POLICY_TYPE_KEY).traverse(p.codec).readValueAs(PolicyType::class.java), - tenantId = jsonNode.get(TENANT_ID_KEY).asText(), + id = requireNotNull(jsonNode.get(POLICY_ID_KEY)) { + "$POLICY_ID_KEY is required!" + }.asText(), + name = requireNotNull(jsonNode.get(POLICY_NAME_KEY)) { + "$POLICY_NAME_KEY is required!" + }.asText(), + category = requireNotNull(jsonNode.get(POLICY_CATEGORY_KEY)) { + "$POLICY_CATEGORY_KEY is required!" + }.asText(), + description = requireNotNull(jsonNode.get(POLICY_DESCRIPTION_KEY)) { + "$POLICY_DESCRIPTION_KEY is required!" + }.asText(), + type = requireNotNull(jsonNode.get(POLICY_TYPE_KEY)) { + "$POLICY_TYPE_KEY is required!" + }.traverse(p.codec).readValueAs(PolicyType::class.java), + tenantId = requireNotNull(jsonNode.get(TENANT_ID_KEY)) { + "$TENANT_ID_KEY is required!" + }.asText(), statements = statements ) } diff --git a/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonStatementSerializer.kt b/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonStatementSerializer.kt index 1b9f464f..02a48e8e 100644 --- a/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonStatementSerializer.kt +++ b/cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonStatementSerializer.kt @@ -26,6 +26,7 @@ import me.ahoo.cosec.api.policy.Effect import me.ahoo.cosec.api.policy.Statement import me.ahoo.cosec.policy.StatementData +const val STATEMENT_NAME = "name" const val STATEMENT_EFFECT_KEY = "effect" const val STATEMENT_ACTIONS_KEY = "actions" const val STATEMENT_CONDITIONS_KEY = "conditions" @@ -33,6 +34,7 @@ const val STATEMENT_CONDITIONS_KEY = "conditions" object JsonStatementSerializer : StdSerializer(Statement::class.java) { override fun serialize(value: Statement, gen: JsonGenerator, provider: SerializerProvider) { gen.writeStartObject() + gen.writeStringField(STATEMENT_NAME, value.name) gen.writePOJOField(STATEMENT_EFFECT_KEY, value.effect) if (value.actions.isNotEmpty()) { gen.writeArrayFieldStart(STATEMENT_ACTIONS_KEY) @@ -55,27 +57,19 @@ object JsonStatementSerializer : StdSerializer(Statement::class.java) object JsonStatementDeserializer : StdDeserializer(Statement::class.java) { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Statement { val jsonNode = p.codec.readTree(p) - val actions = jsonNode.has(STATEMENT_ACTIONS_KEY).let { hasActions -> - if (hasActions) { - jsonNode.get(STATEMENT_ACTIONS_KEY).map { - it.traverse(p.codec).readValueAs(ActionMatcher::class.java) - }.toSet() - } else { - emptySet() - } - } - val conditions = jsonNode.has(STATEMENT_CONDITIONS_KEY).let { hasConditions -> - if (hasConditions) { - jsonNode.get(STATEMENT_CONDITIONS_KEY).map { - it.traverse(p.codec).readValueAs(ConditionMatcher::class.java) - }.toSet() - } else { - emptySet() - } - } + val actions = jsonNode.get(STATEMENT_ACTIONS_KEY)?.map { + it.traverse(p.codec).readValueAs(ActionMatcher::class.java) + }.orEmpty() + + val conditions = jsonNode.get(STATEMENT_CONDITIONS_KEY)?.map { + it.traverse(p.codec).readValueAs(ConditionMatcher::class.java) + }.orEmpty() return StatementData( - effect = jsonNode.get(STATEMENT_EFFECT_KEY).traverse(p.codec).readValueAs(Effect::class.java), + name = jsonNode.get(STATEMENT_NAME)?.asText().orEmpty(), + effect = requireNotNull(jsonNode.get(STATEMENT_EFFECT_KEY)) { + "$STATEMENT_EFFECT_KEY is required!" + }.traverse(p.codec).readValueAs(Effect::class.java), actions = actions, conditions = conditions ) diff --git a/cosec-core/src/test/kotlin/me/ahoo/cosec/authorization/SimpleAuthorizationTest.kt b/cosec-core/src/test/kotlin/me/ahoo/cosec/authorization/SimpleAuthorizationTest.kt index e579d531..c77d07e3 100644 --- a/cosec-core/src/test/kotlin/me/ahoo/cosec/authorization/SimpleAuthorizationTest.kt +++ b/cosec-core/src/test/kotlin/me/ahoo/cosec/authorization/SimpleAuthorizationTest.kt @@ -65,10 +65,10 @@ internal class SimpleAuthorizationTest { fun authorizeWhenGlobalPolicyIsAllowAll() { val globalPolicy = mockk() { every { id } returns "globalPolicy" - every { statements } returns setOf( + every { statements } returns listOf( StatementData( effect = Effect.ALLOW, - actions = setOf(AllActionMatcher(JsonConfiguration.EMPTY)) + actions = listOf(AllActionMatcher(JsonConfiguration.EMPTY)) ) ) } @@ -91,10 +91,10 @@ internal class SimpleAuthorizationTest { fun authorizeWhenGlobalPolicyIsDenyAll() { val globalPolicy = mockk() { every { id } returns "globalPolicy" - every { statements } returns setOf( + every { statements } returns listOf( StatementData( effect = Effect.DENY, - actions = setOf(AllActionMatcher(JsonConfiguration.EMPTY)) + actions = listOf(AllActionMatcher(JsonConfiguration.EMPTY)) ) ) } @@ -116,10 +116,10 @@ internal class SimpleAuthorizationTest { fun authorizeWhenGlobalPolicyIsEmptyAndPrincipalIsAllowAll() { val principalPolicy = mockk() { every { id } returns "policyId" - every { statements } returns setOf( + every { statements } returns listOf( StatementData( effect = Effect.ALLOW, - actions = setOf(AllActionMatcher(JsonConfiguration.EMPTY)) + actions = listOf(AllActionMatcher(JsonConfiguration.EMPTY)) ) ) } @@ -147,10 +147,10 @@ internal class SimpleAuthorizationTest { fun authorizeWhenGlobalPolicyIsEmptyAndPrincipalIsDenyAll() { val principalPolicy = mockk() { every { id } returns "policyId" - every { statements } returns setOf( + every { statements } returns listOf( StatementData( effect = Effect.DENY, - actions = setOf(AllActionMatcher(JsonConfiguration.EMPTY)) + actions = listOf(AllActionMatcher(JsonConfiguration.EMPTY)) ) ) } @@ -178,10 +178,10 @@ internal class SimpleAuthorizationTest { fun authorizeWhenGlobalAndPrincipalPolicyIsEmptyAndRoleIsAllowAll() { val rolePolicy = mockk() { every { id } returns "policyId" - every { statements } returns setOf( + every { statements } returns listOf( StatementData( effect = Effect.ALLOW, - actions = setOf(AllActionMatcher(JsonConfiguration.EMPTY)) + actions = listOf(AllActionMatcher(JsonConfiguration.EMPTY)) ) ) } @@ -210,10 +210,10 @@ internal class SimpleAuthorizationTest { fun authorizeWhenGlobalAndPrincipalPolicyIsEmptyAndRoleIsDenyAll() { val rolePolicy = mockk() { every { id } returns "policyId" - every { statements } returns setOf( + every { statements } returns listOf( StatementData( effect = Effect.DENY, - actions = setOf(AllActionMatcher(JsonConfiguration.EMPTY)) + actions = listOf(AllActionMatcher(JsonConfiguration.EMPTY)) ) ) } diff --git a/cosec-core/src/test/kotlin/me/ahoo/cosec/policy/StatementDataTest.kt b/cosec-core/src/test/kotlin/me/ahoo/cosec/policy/StatementDataTest.kt index 9e9cbbe0..23cf4a56 100644 --- a/cosec-core/src/test/kotlin/me/ahoo/cosec/policy/StatementDataTest.kt +++ b/cosec-core/src/test/kotlin/me/ahoo/cosec/policy/StatementDataTest.kt @@ -33,13 +33,17 @@ internal class StatementDataTest { @Test fun verifyDefault() { val statementData = StatementData() + assertThat(statementData.name, equalTo("")) + assertThat(statementData.effect, equalTo(Effect.ALLOW)) + assertThat(statementData.actions, equalTo(listOf())) + assertThat(statementData.conditions, equalTo(listOf())) assertThat(statementData.verify(mockk(), mockk()), `is`(VerifyResult.IMPLICIT_DENY)) } @Test fun verify() { val statementData = StatementData( - actions = setOf( + actions = listOf( PathActionMatcherFactory().create( mapOf( MATCHER_PATTERN_KEY to "auth/*" @@ -56,14 +60,14 @@ internal class StatementDataTest { @Test fun verifyWithCondition() { val statementData = StatementData( - actions = setOf( + actions = listOf( PathActionMatcherFactory().create( mapOf( MATCHER_PATTERN_KEY to "order/#{principal.id}/*" ).asConfiguration() ) ), - conditions = setOf( + conditions = listOf( SpelConditionMatcherFactory().create( mapOf( MATCHER_PATTERN_KEY to "context.principal.authenticated()" @@ -95,7 +99,7 @@ internal class StatementDataTest { fun verifyDeny() { val statementData = StatementData( effect = Effect.DENY, - actions = setOf(AllActionMatcher(JsonConfiguration.EMPTY)) + actions = listOf(AllActionMatcher(JsonConfiguration.EMPTY)) ) assertThat(statementData.verify(mockk(), mockk()), `is`(VerifyResult.EXPLICIT_DENY)) } diff --git a/cosec-core/src/test/kotlin/me/ahoo/cosec/serialization/CoSecJsonSerializerTest.kt b/cosec-core/src/test/kotlin/me/ahoo/cosec/serialization/CoSecJsonSerializerTest.kt index 0d2a887e..b0b3e20e 100644 --- a/cosec-core/src/test/kotlin/me/ahoo/cosec/serialization/CoSecJsonSerializerTest.kt +++ b/cosec-core/src/test/kotlin/me/ahoo/cosec/serialization/CoSecJsonSerializerTest.kt @@ -51,6 +51,16 @@ import kotlin.streams.toList internal class CoSecJsonSerializerTest { + @Test + fun serializeTestResource() { + val testPolicy = requireNotNull(javaClass.classLoader.getResource("test-policy.json")).let { resource -> + resource.openStream().use { + CoSecJsonSerializer.readValue(it, Policy::class.java) + } + } + assertThat(testPolicy, `is`(notNullValue())) + } + @ParameterizedTest @MethodSource("serializeActionMatcherProvider") fun serializeActionMatcher(actionMatcher: ActionMatcher) { @@ -108,7 +118,7 @@ internal class CoSecJsonSerializerTest { @Test fun serializePolicySet() { val policySetType = object : TypeReference>() {} - val policySet = serializePolicyProvider().toList().toSet() + val policySet = serializePolicyProvider().toList() val output = CoSecJsonSerializer.writeValueAsString(policySet) val input = CoSecJsonSerializer.readValue( output, @@ -252,8 +262,8 @@ internal class CoSecJsonSerializerTest { StatementData(), StatementData( effect = Effect.DENY, - actions = serializeActionMatcherProvider().toList().toSet(), - conditions = serializeConditionMatcherProvider().toList().toSet() + actions = serializeActionMatcherProvider().toList(), + conditions = serializeConditionMatcherProvider().toList() ) ) } @@ -268,7 +278,7 @@ internal class CoSecJsonSerializerTest { description = "", type = PolicyType.CUSTOM, tenantId = "1", - statements = emptySet() + statements = listOf() ), PolicyData( id = "2", @@ -277,7 +287,7 @@ internal class CoSecJsonSerializerTest { description = "", type = PolicyType.SYSTEM, tenantId = "1", - statements = serializeStatementProvider().toList().toSet() + statements = serializeStatementProvider().toList() ) ) } diff --git a/cosec-core/src/test/resources/policy.json b/cosec-core/src/test/resources/test-policy.json similarity index 50% rename from cosec-core/src/test/resources/policy.json rename to cosec-core/src/test/resources/test-policy.json index dc9612f1..1bb622c8 100644 --- a/cosec-core/src/test/resources/policy.json +++ b/cosec-core/src/test/resources/test-policy.json @@ -1,97 +1,93 @@ { - "id": "2", - "name": "auth", - "category": "auth", - "description": "", + "id": "id", + "name": "name", + "category": "category", + "description": "description", "type": "global", - "tenantId": "1", + "tenantId": "tenantId", "statements": [ { + "name": "Anonymous", "effect": "allow", "actions": [ { - "type": "all" + "type": "path", + "pattern": "/auth/register" }, { - "type": "none" - }, + "type": "path", + "pattern": "/auth/login" + } + ] + }, + { + "name": "UserScope", + "effect": "allow", + "actions": [ { "type": "path", - "methods": [ - "GET", - "POST", - "PUT", - "DELETE" - ], - "pattern": "/user/{userId}/*" + "pattern": "/user/#{principal.id}/*" } ], "conditions": [ { "type": "authenticated" - }, + } + ] + }, + { + "name": "Developer", + "effect": "allow", + "actions": [ + { + "type": "all" + } + ], + "conditions": [ { "type": "in", "part": "context.principal.id", "in": [ - "userId" + "developerId" ] - }, - { - "type": "path", - "part": "request.remoteIp", - "path": { - "caseSensitive": false, - "separator": ".", - "decodeAndParseSegments": false - }, - "pattern": "192.168.0.*" } ] }, { + "name": "RequestOriginDeny", "effect": "deny", "actions": [ { - "type": "all", - "methods": [ - "GET" - ] - }, - { - "type": "none" - }, - { - "type": "path", - "pattern": ".*" - }, - { - "type": "path", - "pattern": "#{principal.id}.*" - }, + "type": "all" + } + ], + "conditions": [ { "type": "reg", - "pattern": ".*" - }, + "negate": true, + "part": "request.origin", + "pattern": "^(http|https)://github.com" + } + ] + }, + { + "name": "IpBlacklist", + "effect": "deny", + "actions": [ { - "type": "reg", - "pattern": "#{principal.id}.*" + "type": "all" } ], "conditions": [ { - "type": "all" - }, - { - "type": "none" - }, - { - "type": "spel", - "pattern": "context.principal.id=='1'" - }, - { - "type": "ognl", - "pattern": "path == \"auth/login\"" + "type": "path", + "part": "request.remoteIp", + "path": { + "caseSensitive": false, + "separator": ".", + "decodeAndParseSegments": false + }, + "pattern": "192.168.0.*" } ] } diff --git a/cosec-gateway-server/src/main/resources/application.yaml b/cosec-gateway-server/src/main/resources/application.yaml index 0702ee88..106b29d6 100644 --- a/cosec-gateway-server/src/main/resources/application.yaml +++ b/cosec-gateway-server/src/main/resources/application.yaml @@ -52,3 +52,4 @@ cosec: logging: level: root: info + me.ahoo.cosec.authorization.SimpleAuthorization: debug diff --git a/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/PolicyCodecExecutorTest.kt b/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/PolicyCodecExecutorTest.kt index 31d06b7a..5964e398 100644 --- a/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/PolicyCodecExecutorTest.kt +++ b/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/PolicyCodecExecutorTest.kt @@ -63,7 +63,7 @@ internal class PolicyCodecExecutorTest { description = "", type = PolicyType.SYSTEM, tenantId = "1", - statements = setOf(StatementData()) + statements = listOf(StatementData()) ) val roleId = MockIdGenerator.INSTANCE.generateAsString() val cacheValue: CacheValue = CacheValue.forever(policy) diff --git a/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/RedisPolicyRepositoryTest.kt b/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/RedisPolicyRepositoryTest.kt index d1e51779..e967ab71 100644 --- a/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/RedisPolicyRepositoryTest.kt +++ b/cosec-redis/src/test/kotlin/me/ahoo/cosec/redis/RedisPolicyRepositoryTest.kt @@ -29,7 +29,7 @@ internal class RedisPolicyRepositoryTest { "policyType", PolicyType.SYSTEM, "tenantId", - emptySet() + listOf() ) @Test diff --git a/document/cosec-policy.schema.json b/document/cosec-policy.schema.json index da960f48..194ffe35 100644 --- a/document/cosec-policy.schema.json +++ b/document/cosec-policy.schema.json @@ -211,6 +211,10 @@ "statement": { "type": "object", "properties": { + "name": { + "description": "The name of the Statement", + "type": "string" + }, "effect": { "$ref": "#/definitions/effect" }, diff --git a/document/design/assets/Modeling.svg b/document/design/assets/Modeling.svg index 1ac60ee1..eb7c1713 100644 --- a/document/design/assets/Modeling.svg +++ b/document/design/assets/Modeling.svg @@ -11,62 +11,62 @@ - limitations under the License. --> -https://github.com/Ahoo-Wang/CoSecCoSec Modeling Class Diagramtenantpolicyprincipaljava.securitycontextauthorizationauthenticationTenantval tenantId: Stringval isPlatform: BooleanTenantCapableval tenant: TenantIs it a root platform tenant?PolicyTypeSYSTEMCUSTOMGLOBALPolicyval id: Stringval name: Stringval type: PolicyTypeval description: Stringval tenantId:Stringval statements: Set<Statement>EffectALLOWDENYRequestMatchermatch(Request,SecurityContext): BooleanActionMatcherConditionMatcherVerifyResultALLOWEXPLICIT_DENYIMPLICIT_DENYStatementval effect: Effectval actions: Set<ActionMatcher>val conditions: Set<ConditionMatcher>PermissionVerifierverify(Request,SecurityContext): VerifyResultPolicyEvaluatorevaluate(Policy)PolicyCapableval policies: Set<String>Used to evaluate the effectiveness of the PolicyRoleCapableval roles: Set<String>CoSecPrincipalval id: Stringval attributes: Map<String, String>anonymous(): Booleanauthenticated(): BooleanTenantPrincipalA set of rolesPrincipalgetName():StringSecurityContextval principal: CoSecPrincipalsetAttribute(String, Object): SecurityContextgetAttribute(String): T?getRequiredAttribute(String): TRequestval path: Stringval method: Stringval remoteIp: Stringval origin: Stringval referer: StringgetHeader(key: String): StringAuthorizeResultval authorized: Booleanval reason: StringAuthorizationauthorize(Request,SecurityContext): Mono<AuthorizeResult>CredentialsAuthenticationC:Credentials,P:CoSecPrincipalsupportCredentials: Class<C>authenticate(C): Mono<P>AuthenticationProviderget(Class<Credentials>): A?getRequired(Class<Credentials>): Aregister(A): Unitregister(Class<C>, A): Unitregister(A): UnitgetRequired(Class<Credentials>): ANamedname: Stringhttps://github.com/Ahoo-Wang/CoSectenantpolicyprincipaljava.securitycontextauthorizationauthenticationTenantval tenantId: Stringval isPlatformTenant: Booleanval isDefaultTenant: Booleanval isUserTenant: BooleanTenantCapableval tenant: TenantIs it a root platform tenant?PolicyTypeSYSTEMCUSTOMGLOBALPolicyval id: Stringval name: Stringval type: PolicyTypeval description: Stringval tenantId:Stringval statements: Set<Statement>EffectALLOWDENYRequestMatchermatch(Request,SecurityContext): BooleanActionMatcherConditionMatcherVerifyResultALLOWEXPLICIT_DENYIMPLICIT_DENYStatementval name: Stringval effect: Effectval actions: Set<ActionMatcher>val conditions: Set<ConditionMatcher>PermissionVerifierverify(Request,SecurityContext): VerifyResultPolicyEvaluatorevaluate(Policy)PolicyCapableval policies: Set<String>Used to evaluate the effectiveness of the PolicyRoleCapableval roles: Set<String>CoSecPrincipalval id: Stringval attributes: Map<String, String>anonymous(): Booleanauthenticated(): BooleanTenantPrincipalA set of rolesPrincipalgetName():StringSecurityContextval principal: CoSecPrincipalsetAttribute(String, Object): SecurityContextgetAttribute(String): T?getRequiredAttribute(String): TRequestval path: Stringval method: Stringval remoteIp: Stringval origin: Stringval referer: StringgetHeader(key: String): StringAuthorizeResultval authorized: Booleanval reason: StringAuthorizationauthorize(Request,SecurityContext): Mono<AuthorizeResult>CredentialsAuthenticationC:Credentials,P:CoSecPrincipalsupportCredentials: Class<C>authenticate(C): Mono<P>AuthenticationProviderget(Class<Credentials>): A?getRequired(Class<Credentials>): Aregister(A): Unitregister(Class<C>, A): Unitregister(A): UnitgetRequired(Class<Credentials>): ANamedname: Stringhttps://github.com/Ahoo-Wang/CoSec