package fr.dcproject.common.security /** Responses of AccessControl */ enum class AccessDecision { GRANTED, DENIED; /** * Convert decision to boolean */ fun toBoolean(): Boolean = when (this) { GRANTED -> true DENIED -> false } } abstract class AccessControl { /** * A Shortcut for return a GrantedResponse */ protected fun granted(message: String? = null, code: String? = null): GrantedResponse = GrantedResponse(this, message, code) /** * A Shortcut for return a DeniedResponse */ protected fun denied(message: String, code: String): DeniedResponse = DeniedResponse(this, message, code) /** * Check all responses and return DENIED if one is DENIED * * If the list of responses is empty, return GRANTED */ private fun AccessResponses.getOneResponse(): AccessResponse = this.firstOrNull { it.decision == AccessDecision.DENIED } ?: granted() /** * An helper to convert a list of subject into one response */ protected fun , T> canAll(items: S, action: (T) -> AccessResponse): AccessResponse = items .map { action(it) } .getOneResponse() } /** * Throw an Exception if AccessControl return a DENIED response */ fun T.assert(action: T.() -> AccessResponse) { action().assert() } /** * Check all responses and return DENIED if one is DENIED * * If the list of responses is empty, return GRANTED */ fun AccessResponses.getOneResponse(): AccessResponse = this.firstOrNull { it.decision == AccessDecision.DENIED } ?: GrantedResponse(first().accessControl) /** * Throw an Exception if one response is DENIED */ fun AccessResponses.assert() = this.getOneResponse().assert() class AccessDeniedException(private val accessResponses: AccessResponses) : Throwable(accessResponses.first().message) { constructor(accessResponse: AccessResponse) : this(listOf(accessResponse)) /** * Get first response */ fun first(): AccessResponse = accessResponses.first() /** * Check if the error code is present into the responses */ fun hasErrorCode(code: String): Boolean = accessResponses .filter { it.decision == AccessDecision.DENIED } .any { it.code == code } /** * Find and return the response than match with the error code */ fun getErrorCode(code: String): AccessResponse? = accessResponses .firstOrNull { it.decision == AccessDecision.DENIED && it.code == code } /** * Get a list of messages of all responses */ fun getMessages(): List = accessResponses .mapNotNull { it.message } /** * Get the first message */ fun getFirstMessage(): String? = accessResponses .first() .message } /** * The response that all AccessControl method return * @see GrantedResponse * @see DeniedResponse */ sealed class AccessResponse( val decision: AccessDecision, val accessControl: AccessControl, val message: String?, val code: String? ) { /** * Convert response as boolean */ fun toBoolean(): Boolean = decision.toBoolean() /** * Throw Exception if response if DENIED */ fun assert() { if (this.decision == AccessDecision.DENIED) { throw AccessDeniedException(this) } } } class GrantedResponse( accessControl: AccessControl, message: String? = null, code: String? = null ) : AccessResponse(AccessDecision.GRANTED, accessControl, message, code) class DeniedResponse( accessControl: AccessControl, message: String, code: String ) : AccessResponse(AccessDecision.DENIED, accessControl, message, code) typealias AccessResponses = List