Skip to content

Commit

Permalink
Fix DuplicatePluginException, Add tests
Browse files Browse the repository at this point in the history
Closes #2
  • Loading branch information
omkar-tenkale committed Jan 16, 2023
1 parent eb62189 commit a4025b1
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 75 deletions.
Binary file removed .DS_Store
Binary file not shown.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
.gradle
build
build
.DS_Store
25 changes: 13 additions & 12 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@ plugins {
}

group = "io.github.omkar-tenkale"
version = "0.1.0"
version = "0.2.0"

repositories {
mavenCentral()
}

dependencies {

val ktorVersion = "2.1.3"
implementation("io.ktor:ktor-server-core-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-webjars:$ktorVersion")
implementation("io.ktor:ktor-server-auth:$ktorVersion")
implementation("io.ktor:ktor-server-auth-jwt-jvm:$ktorVersion")
testImplementation("io.ktor:ktor-server-netty-jvm:$ktorVersion")
testImplementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
testImplementation("io.ktor:ktor-serialization-jackson:$ktorVersion")
testImplementation("io.ktor:ktor-server-auth:$ktorVersion")
testImplementation("io.ktor:ktor-server-call-logging:$ktorVersion")
val ktor_version = "2.2.1"
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-webjars:$ktor_version")
implementation("io.ktor:ktor-server-auth:$ktor_version")
testImplementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
testImplementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-server-status-pages:$ktor_version")
testImplementation("io.ktor:ktor-serialization-jackson:$ktor_version")
testImplementation("io.ktor:ktor-server-auth:$ktor_version")
testImplementation("io.ktor:ktor-server-call-logging:$ktor_version")
testImplementation("io.ktor:ktor-server-test-host:$ktor_version")
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,92 +6,114 @@ import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*

typealias Role = String
class RoleBasedAuthConfiguration {
var requiredRoles: Set<String> = emptySet()
lateinit var authType: AuthType
}
enum class AuthType {
ALL,
ANY,
NONE,
}
class AuthorizedRouteSelector(private val description: String) : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Constant

class RoleBasedAuthConfiguration(
var any: Set<Role>? = null,
var all: Set<Role>? = null,
var none: Set<Role>? = null,
)
override fun toString(): String = "(authorize ${description})"
}

fun Route.withRole(role: Role, build: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) =
withAnyRole(setOf(role), build)
class RoleBasedAuthPluginConfiguration {
var roleExtractor: ((Principal) -> Set<Role>) = { emptySet() }
private set

fun Route.withAllRoles(roles: Set<Role>, build: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) {
install(RoleBasedAuthPlugin) {
all = roles.toSet()
fun extractRoles(extractor: (Principal) -> Set<Role>) {
roleExtractor = extractor
}
handle { build() }
var throwErrorOnUnauthorizedResponse = false
}

fun Route.withoutRoles(roles: Set<Role>, build: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) {
install(RoleBasedAuthPlugin) {
none = roles.toSet()
}
handle { build() }
private lateinit var pluginGlobalConfig: RoleBasedAuthPluginConfiguration
fun AuthenticationConfig.roleBased(config:RoleBasedAuthPluginConfiguration.()->Unit){
pluginGlobalConfig = RoleBasedAuthPluginConfiguration().apply(config)
}

fun Route.withAnyRole(roles: Set<Role>, build: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) {
install(RoleBasedAuthPlugin) {
any = roles.toSet()
private fun Route.buildAuthorizedRoute(
requiredRoles: Set<Role>,
authType: AuthType,
build: Route.() -> Unit
): Route {
val authorizedRoute = createChild(AuthorizedRouteSelector(requiredRoles.joinToString(",")))
authorizedRoute.install(RoleBasedAuthPlugin) {
this.requiredRoles = requiredRoles
this.authType = authType
}
handle { build() }
authorizedRoute.build()
return authorizedRoute
}
fun Route.withRole(role: Role, build: Route.() -> Unit) =
buildAuthorizedRoute(requiredRoles = setOf(role),authType= AuthType.ALL, build = build)

fun Route.withRoles(vararg roles: Role, build: Route.() -> Unit) =
buildAuthorizedRoute(requiredRoles = roles.toSet(),authType= AuthType.ALL, build = build)

fun Route.withAnyRole(vararg roles: Role, build: Route.() -> Unit) =
buildAuthorizedRoute(requiredRoles = roles.toSet(),authType = AuthType.ANY, build = build)

fun Route.withoutRoles(vararg roles: Role, build: Route.() -> Unit) =
buildAuthorizedRoute(requiredRoles = roles.toSet(), authType = AuthType.NONE,build = build)


val RoleBasedAuthPlugin =
createRouteScopedPlugin(name = "RoleBasedAuthorization", createConfiguration = ::RoleBasedAuthConfiguration) {
if(::pluginGlobalConfig.isInitialized.not()){
error("RoleBasedAuthPlugin not initialized. Setup plugin by calling AuthenticationConfig#roleBased in authenticate block")
}
with(pluginConfig) {
on(AuthenticationChecked) { call ->
val principal = call.principal<Principal>() ?: error("Missing principal")
val roles = roleBasedAuthPluginConfiguration?.roleExtractor?.invoke(principal)
?: error("RoleBasedAuthPlugin is not initialized,You can initialize it by calling 'installRoleBasedAuthPlugin()'")
val principal = call.principal<Principal>() ?: return@on
val userRoles = pluginGlobalConfig.roleExtractor.invoke(principal)
val denyReasons = mutableListOf<String>()
all?.let {
val missing = it - roles
if (missing.isNotEmpty()) {
denyReasons += "Principal $principal lacks required role(s) ${missing.joinToString(" and ")}"

when(authType) {
AuthType.ALL -> {
val missing = requiredRoles - userRoles
if (missing.isNotEmpty()) {
denyReasons += "Principal lacks required role(s) ${missing.joinToString(" and ")}"
}
}
}
any?.let {
if (it.none { it in roles }) {
denyReasons += "Principal $principal has none of the sufficient role(s) ${
it.joinToString(
" or "
)
}"
AuthType.ANY -> {
if (userRoles.none { it in requiredRoles }) {
denyReasons += "Principal has none of the sufficient role(s) ${
requiredRoles.joinToString(
" or "
)
}"
}
}
}
none?.let {
if (it.any { it in roles }) {
denyReasons += "Principal $principal has forbidden role(s) ${
(it.intersect(roles)).joinToString(
" and "
)
}"
AuthType.NONE -> {
if (userRoles.any{ it in requiredRoles}) {
denyReasons += "Principal has forbidden role(s) ${
(requiredRoles.intersect(userRoles)).joinToString(
" and "
)
}"

}
}
}
if (denyReasons.isNotEmpty()) {
val message = denyReasons.joinToString(". ")
println("Authorization failed for ${call.request.path()}. $message")
call.respond(HttpStatusCode.Forbidden)
if(pluginGlobalConfig.throwErrorOnUnauthorizedResponse){
throw UnauthorizedAccessException(denyReasons)
}else{
val message = denyReasons.joinToString(". ")
if(application.developmentMode){
application.log.warn("Authorization failed for ${call.request.path()} $message")
}
call.respond(HttpStatusCode.Forbidden)
}
}
}
}
}

private var roleBasedAuthPluginConfiguration: RoleBasedAuthPluginConfiguration? = null
fun Application.installRoleBasedAuthPlugin(configuration: RoleBasedAuthPluginConfiguration.() -> Unit) {
roleBasedAuthPluginConfiguration = RoleBasedAuthPluginConfiguration().apply { configuration() }
}


class RoleBasedAuthPluginConfiguration {
var roleExtractor: ((Principal) -> Set<Role>)? = null
private set

fun extractRoles(extractor: (Principal) -> Set<Role>) {
roleExtractor = extractor
}
}
class UnauthorizedAccessException(val denyReasons: MutableList<String>) : Exception()
Loading

0 comments on commit a4025b1

Please sign in to comment.