Skip to content

Commit

Permalink
Feature: support VerifyContext. (#90)
Browse files Browse the repository at this point in the history
* Feature: support `VerifyContext`.
  • Loading branch information
Ahoo-Wang authored Feb 22, 2023
1 parent 634c4a7 commit 49a8d46
Show file tree
Hide file tree
Showing 21 changed files with 339 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,25 @@ package me.ahoo.cosec.api.context
import me.ahoo.cosec.api.principal.CoSecPrincipal
import me.ahoo.cosec.api.tenant.TenantCapable

interface SecurityContext : TenantCapable, Attributes<SecurityContext, String, Any> {
interface SecurityContext : TenantCapable {
companion object {
const val KEY = "COSEC_SECURITY_CONTEXT"
}

val attributes: MutableMap<String, Any>
val principal: CoSecPrincipal

fun <V> getAttributeValue(attributeKey: String): V? {
@Suppress("UNCHECKED_CAST")
return attributes[attributeKey] as V?
}

fun <V> getRequiredAttributeValue(attributeKey: String): V {
return requireNotNull(getAttributeValue(attributeKey))
}

fun setAttributeValue(attributeKey: String, value: Any): SecurityContext {
attributes[attributeKey] = value
return this
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.api.context

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test

class AttributesTest {

@Test
fun mergeAttributes() {
val attributes = MockAttributes().mergeAttributes(additionalAttributes = mapOf("a" to "a"))
assertThat(attributes.attributes["a"], equalTo("a"))
assertThat(attributes, equalTo(attributes.mergeAttributes(emptyMap())))
}

data class MockAttributes(override val attributes: Map<String, String> = emptyMap()) :
Attributes<MockAttributes, String, String> {
override fun withAttributes(attributes: Map<String, String>): MockAttributes {
return copy(attributes = attributes)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import me.ahoo.cosec.api.policy.Policy
import me.ahoo.cosec.api.policy.Statement
import me.ahoo.cosec.api.policy.VerifyResult
import me.ahoo.cosec.api.principal.CoSecPrincipal.Companion.isRoot
import me.ahoo.cosec.authorization.VerifyContext.Companion.setVerifyContext
import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
import reactor.kotlin.core.publisher.toMono

/**
Expand All @@ -35,7 +37,7 @@ class SimpleAuthorization(private val policyRepository: PolicyRepository) : Auth
private val log = LoggerFactory.getLogger(SimpleAuthorization::class.java)
}

private fun verifyPolicies(policies: Set<Policy>, request: Request, context: SecurityContext): VerifyResult {
private fun verifyPolicies(policies: Set<Policy>, request: Request, context: SecurityContext): VerifyContext? {
policies.forEach { policy: Policy ->
policy.statements.filter { statement: Statement ->
statement.effect == Effect.DENY
Expand All @@ -47,7 +49,12 @@ class SimpleAuthorization(private val policyRepository: PolicyRepository) : Auth
"Verify [$request] [$context] matched Policy[${policy.id}] Statement[$index][${statement.name}] - [Explicit Deny].",
)
}
return VerifyResult.EXPLICIT_DENY
return VerifyContext(
policy = policy,
statementIndex = index,
statement = statement,
result = verifyResult
)
}
}
}
Expand All @@ -63,12 +70,19 @@ class SimpleAuthorization(private val policyRepository: PolicyRepository) : Auth
"Verify [$request] [$context] matched Policy[${policy.id}] Statement[$index][${statement.name}] - [Allow].",
)
}
return VerifyResult.ALLOW
return VerifyContext(
policy = policy,
statementIndex = index,
statement = statement,
result = verifyResult
)
}
}
}

return VerifyResult.IMPLICIT_DENY
/**
* [VerifyResult.IMPLICIT_DENY]
*/
return null
}

private fun verifyRoot(context: SecurityContext): VerifyResult {
Expand All @@ -82,32 +96,29 @@ class SimpleAuthorization(private val policyRepository: PolicyRepository) : Auth
}
}

private fun verifyGlobalPolicies(request: Request, context: SecurityContext): Mono<VerifyResult> {
private fun verifyGlobalPolicies(request: Request, context: SecurityContext): Mono<VerifyContext> {
return policyRepository.getGlobalPolicy()
.defaultIfEmpty(emptySet())
.map { policies: Set<Policy> ->
.mapNotNull { policies: Set<Policy> ->
verifyPolicies(policies, request, context)
}
}

private fun verifyPrincipalPolicies(request: Request, context: SecurityContext): Mono<VerifyResult> {
private fun verifyPrincipalPolicies(request: Request, context: SecurityContext): Mono<VerifyContext> {
if (context.principal.policies.isEmpty()) {
return VerifyResult.IMPLICIT_DENY.toMono()
return Mono.empty()
}
return policyRepository.getPolicies(context.principal.policies)
.defaultIfEmpty(emptySet())
.map { policies: Set<Policy> ->
.mapNotNull { policies: Set<Policy> ->
verifyPolicies(policies, request, context)
}
}

private fun verifyRolePolicies(request: Request, context: SecurityContext): Mono<VerifyResult> {
private fun verifyRolePolicies(request: Request, context: SecurityContext): Mono<VerifyContext> {
if (context.principal.roles.isEmpty()) {
return VerifyResult.IMPLICIT_DENY.toMono()
return Mono.empty()
}
return policyRepository.getRolePolicy(context.principal.roles)
.defaultIfEmpty(emptySet())
.map { policies: Set<Policy> ->
.mapNotNull { policies: Set<Policy> ->
verifyPolicies(policies, request, context)
}
}
Expand All @@ -119,34 +130,26 @@ class SimpleAuthorization(private val policyRepository: PolicyRepository) : Auth
}

return verifyGlobalPolicies(request, context)
.flatMap { globalVerifyResult: VerifyResult ->
if (globalVerifyResult == VerifyResult.IMPLICIT_DENY) {
return@flatMap verifyPrincipalPolicies(request, context)
}
globalVerifyResult.toMono()
.switchIfEmpty {
verifyPrincipalPolicies(request, context)
}
.flatMap { principalVerifyResult: VerifyResult ->
when (principalVerifyResult) {
VerifyResult.ALLOW -> AuthorizeResult.ALLOW.toMono()
VerifyResult.EXPLICIT_DENY -> AuthorizeResult.EXPLICIT_DENY.toMono()
VerifyResult.IMPLICIT_DENY -> {
verifyRolePolicies(request, context)
.map { roleVerifyResult: VerifyResult ->
when (roleVerifyResult) {
VerifyResult.ALLOW -> AuthorizeResult.ALLOW
VerifyResult.EXPLICIT_DENY -> AuthorizeResult.EXPLICIT_DENY
VerifyResult.IMPLICIT_DENY -> {
if (log.isDebugEnabled) {
log.debug(
"Verify [$request] [$context] No policies matched - [Implicit Deny].",
)
}
AuthorizeResult.IMPLICIT_DENY
}
}
}
}
.switchIfEmpty {
verifyRolePolicies(request, context)
}
.map {
context.setVerifyContext(it)
when (it.result) {
VerifyResult.ALLOW -> AuthorizeResult.ALLOW
VerifyResult.EXPLICIT_DENY -> AuthorizeResult.EXPLICIT_DENY
VerifyResult.IMPLICIT_DENY -> throw IllegalStateException("VerifyResult.IMPLICIT_DENY")
}
}.switchIfEmpty {
if (log.isDebugEnabled) {
log.debug(
"Verify [$request] [$context] No policies matched - [Implicit Deny].",
)
}
AuthorizeResult.IMPLICIT_DENY.toMono()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.authorization

import me.ahoo.cosec.api.context.SecurityContext
import me.ahoo.cosec.api.policy.Policy
import me.ahoo.cosec.api.policy.Statement
import me.ahoo.cosec.api.policy.VerifyResult

data class VerifyContext(
val policy: Policy,
val statementIndex: Int,
val statement: Statement,
val result: VerifyResult
) {
companion object {
private const val KEY = "COSEC_AUTHORIZATION_VERIFY_CONTEXT"

fun SecurityContext.setVerifyContext(verifyContext: VerifyContext) {
this.setAttributeValue(KEY, verifyContext)
}

fun SecurityContext.getVerifyContext(): VerifyContext? {
return this.getAttributeValue(KEY)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import me.ahoo.cosec.api.token.AccessToken
abstract class AbstractSecurityContextParser<R> :
SecurityContextParser<R> {
override fun parse(request: R): SecurityContext {
val accessToken = getAccessToken(request) ?: return SimpleSecurityContext.ANONYMOUS
val accessToken = getAccessToken(request) ?: return SimpleSecurityContext.anonymous()
val principal = asPrincipal(accessToken)
return SimpleSecurityContext(principal)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ fun interface SecurityContextParser<R> {
if (LOG.isDebugEnabled) {
LOG.debug(ignored.message, ignored)
}
SimpleSecurityContext.ANONYMOUS
SimpleSecurityContext.anonymous()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import me.ahoo.cosec.api.tenant.Tenant
import me.ahoo.cosec.api.tenant.TenantCapable
import me.ahoo.cosec.principal.SimpleTenantPrincipal
import me.ahoo.cosec.tenant.SimpleTenant
import java.util.concurrent.ConcurrentHashMap
import javax.annotation.concurrent.ThreadSafe

/**
Expand All @@ -29,19 +30,14 @@ import javax.annotation.concurrent.ThreadSafe
class SimpleSecurityContext(
override val principal: CoSecPrincipal,
override val tenant: Tenant = principal.tenant,
override val attributes: Map<String, Any> = emptyMap(),
override val attributes: MutableMap<String, Any> = ConcurrentHashMap(),
) : SecurityContext {
companion object {
val ANONYMOUS: SecurityContext = SimpleSecurityContext(SimpleTenantPrincipal.ANONYMOUS)
fun anonymous(): SecurityContext {
return SimpleSecurityContext(SimpleTenantPrincipal.ANONYMOUS)
}
}

override fun withAttributes(attributes: Map<String, Any>): SecurityContext =
SimpleSecurityContext(
principal = principal,
tenant = tenant,
attributes = attributes,
)

override fun toString(): String {
return "SimpleSecurityContext(principal.id=${principal.id}, tenantId=${tenant.tenantId})"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal class SimpleAuthorizationTest {
val request = mockk<Request> {
}

authorization.authorize(request, SimpleSecurityContext.ANONYMOUS)
authorization.authorize(request, SimpleSecurityContext.anonymous())
.test()
.expectNext(AuthorizeResult.IMPLICIT_DENY)
.verifyComplete()
Expand All @@ -81,7 +81,7 @@ internal class SimpleAuthorizationTest {
val request = mockk<Request> {
}

authorization.authorize(request, SimpleSecurityContext.ANONYMOUS)
authorization.authorize(request, SimpleSecurityContext.anonymous())
.test()
.expectNext(AuthorizeResult.ALLOW)
.verifyComplete()
Expand All @@ -106,7 +106,7 @@ internal class SimpleAuthorizationTest {
val authorization = SimpleAuthorization(policyRepository)
val request = mockk<Request>()

authorization.authorize(request, SimpleSecurityContext.ANONYMOUS)
authorization.authorize(request, SimpleSecurityContext.anonymous())
.test()
.expectNext(AuthorizeResult.EXPLICIT_DENY)
.verifyComplete()
Expand All @@ -123,10 +123,11 @@ internal class SimpleAuthorizationTest {
),
)
}
val securityContext = mockk<SecurityContext>() {
val securityContext = mockk<SecurityContext> {
every { principal.authenticated() } returns false
every { principal.id } returns ""
every { principal.policies } returns setOf("principalPolicy")
every { setAttributeValue(any(), any()) } returns this
}
val policyRepository = mockk<PolicyRepository>() {
every { getGlobalPolicy() } returns Mono.empty()
Expand Down Expand Up @@ -158,8 +159,9 @@ internal class SimpleAuthorizationTest {
every { principal.authenticated() } returns false
every { principal.id } returns ""
every { principal.policies } returns setOf("principalPolicy")
every { setAttributeValue(any(), any()) } returns this
}
val policyRepository = mockk<PolicyRepository>() {
val policyRepository = mockk<PolicyRepository> {
every { getGlobalPolicy() } returns Mono.empty()
every { getPolicies(any()) } returns Mono.just(setOf(principalPolicy))
every { getRolePolicy(any()) } returns Mono.empty()
Expand All @@ -185,13 +187,14 @@ internal class SimpleAuthorizationTest {
),
)
}
val securityContext = mockk<SecurityContext>() {
val securityContext = mockk<SecurityContext> {
every { principal.authenticated() } returns false
every { principal.id } returns ""
every { principal.policies } returns emptySet()
every { principal.roles } returns setOf("rolePolicy")
every { setAttributeValue(any(), any()) } returns this
}
val policyRepository = mockk<PolicyRepository>() {
val policyRepository = mockk<PolicyRepository> {
every { getGlobalPolicy() } returns Mono.empty()
every { getPolicies(any()) } returns Mono.empty()
every { getRolePolicy(any()) } returns Mono.just(setOf(rolePolicy))
Expand All @@ -217,11 +220,12 @@ internal class SimpleAuthorizationTest {
),
)
}
val securityContext = mockk<SecurityContext>() {
val securityContext = mockk<SecurityContext> {
every { principal.authenticated() } returns false
every { principal.id } returns ""
every { principal.policies } returns emptySet()
every { principal.roles } returns setOf("rolePolicy")
every { setAttributeValue(any(), any()) } returns this
}
val policyRepository = mockk<PolicyRepository>() {
every { getGlobalPolicy() } returns Mono.empty()
Expand Down
Loading

0 comments on commit 49a8d46

Please sign in to comment.