konrad-kaminski / spring-kotlin-coroutine

Kotlin coroutine support for Spring.
448 stars 69 forks source link

spring security context not propagated #19

Closed userquin closed 4 years ago

userquin commented 6 years ago

How can I access/configure SecurityContext inside suspend function?

In this example, UserService::changePassword method access to SecurityContextHolder.getContext(): it is null when controller method has the suspend modifier (changePasswordNotWorking) while default method is working (changePassword). I suppose it is also applicable when service has security annotations:

@RestController
@Coroutine(COMMON_POOL)
class UserController(
  val userService: UserService
) {
  @PostMapping(
            "/api/change-password-working",
            consumes = [MediaType.APPLICATION_JSON_UTF8_VALUE],
            produces = [MediaType.APPLICATION_JSON_UTF8_VALUE]
  )
  fun changePassword(@RequestBody body: MutableMap<String, Any?>): SomeObject = userService.changePassword(body)
  @PostMapping(
            "/api/change-password-not-working",
            consumes = [MediaType.APPLICATION_JSON_UTF8_VALUE],
            produces = [MediaType.APPLICATION_JSON_UTF8_VALUE]
  )
  suspend fun changePasswordNotWorking(@RequestBody body: MutableMap<String, Any?>): SomeObject = userService.changePassword(body)
}
userquin commented 6 years ago

ok, a first version could be (can be integrated in the project?):

const val SECURED_COMMON_POOL = "SecuredCommonPool"
const val SECURED_UNCONFINED = "SecuredUnconfined"

internal open class SpringSecurityCoroutineContext(
        private val dispatcher: ContinuationInterceptor
): AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    private var securityContext: SecurityContext = SecurityContextHolder.getContext()
    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = dispatcher.interceptContinuation(Wrapper(continuation))
    inner class Wrapper<T>(private val continuation: Continuation<T>): Continuation<T> {
        private inline fun wrap(block: () -> Unit) {
            try {
                SecurityContextHolder.setContext(securityContext)
                block()
            } finally {
                securityContext = SecurityContextHolder.getContext()
                SecurityContextHolder.clearContext()
            }
        }

        override val context: CoroutineContext get() = continuation.context
        override fun resume(value: T) = wrap { continuation.resume(value) }
        override fun resumeWithException(exception: Throwable) = wrap { continuation.resumeWithException(exception) }
    }

}

internal open class SpringSecurityCoroutineContextResolver: CoroutineContextResolver {
    override fun resolveContext(beanName: String, bean: Any?): CoroutineContext? = when(beanName) {
        SECURED_COMMON_POOL -> SpringSecurityCoroutineContext(CommonPool)
        SECURED_UNCONFINED -> SpringSecurityCoroutineContext(Unconfined)
        else -> bean as? CoroutineContext
    }
}

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
open class CoroutineContextResolverConfiguration {
  ...
  ..
  @Bean
  @ConditionalOnClass("org.springframework.security.core.context.SecurityContext")
  open fun springSecurityCoroutineContextResolver(): CoroutineContextResolver = 
    SpringSecurityCoroutineContextResolver()
}
userquin commented 6 years ago

Previous code will not work just because the context is cached by CoroutineMethodInterceptor, so all calls will use the same SpringSecurityCoroutineContext.

val contextKey = coroutine.context to coroutine.name
val context = contextMap[contextKey] ?: getContext(contextKey).apply { contextMap[contextKey] = this }

so the solution is to capture SecurityContext in the interceptor:

Just move private var securityContext: SecurityContext = SecurityContextHolder.getContext() from SpringSecurityCoroutineContext to Wrapper.

const val SECURED_COMMON_POOL = "SecuredCommonPool"
const val SECURED_UNCONFINED = "SecuredUnconfined"

internal open class SpringSecurityCoroutineContext(
        private val dispatcher: ContinuationInterceptor
): AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = dispatcher.interceptContinuation(Wrapper(continuation))
    inner class Wrapper<T>(private val continuation: Continuation<T>): Continuation<T> {
        private var securityContext: SecurityContext = SecurityContextHolder.getContext()
        private inline fun wrap(block: () -> Unit) {
            try {
                SecurityContextHolder.setContext(securityContext)
                block()
            } finally {
                securityContext = SecurityContextHolder.getContext()
                SecurityContextHolder.clearContext()
            }
        }

        override val context: CoroutineContext get() = continuation.context
        override fun resume(value: T) = wrap { continuation.resume(value) }
        override fun resumeWithException(exception: Throwable) = wrap { continuation.resumeWithException(exception) }
    }

}

internal open class SpringSecurityCoroutineContextResolver: CoroutineContextResolver {
    override fun resolveContext(beanName: String, bean: Any?): CoroutineContext? = when(beanName) {
        SECURED_COMMON_POOL -> SpringSecurityCoroutineContext(CommonPool)
        SECURED_UNCONFINED -> SpringSecurityCoroutineContext(Unconfined)
        else -> bean as? CoroutineContext
    }
}

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
open class CoroutineContextResolverConfiguration {
  ...
  ..
  @Bean
  @ConditionalOnClass("org.springframework.security.core.context.SecurityContext")
  open fun springSecurityCoroutineContextResolver(): CoroutineContextResolver = 
    SpringSecurityCoroutineContextResolver()
}
dndll commented 4 years ago

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL)