diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 2a008bd2..a5840fc5 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation("io.ktor:ktor-server-auth:$ktorVersion") implementation("io.ktor:ktor-server-auth-jwt:$ktorVersion") implementation("io.ktor:ktor-server-sessions:$ktorVersion") + implementation("io.ktor:ktor-server-call-logging:$ktorVersion") implementation("ch.qos.logback:logback-classic:$logbackVersion") diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/Application.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/Application.kt index 4531f337..28ae37dc 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/Application.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/Application.kt @@ -5,6 +5,7 @@ import io.ktor.util.logging.KtorSimpleLogger import org.tod87et.roomkn.server.plugins.configureAuthentication import org.tod87et.roomkn.server.plugins.configureCORS import org.tod87et.roomkn.server.plugins.configureCleanup +import org.tod87et.roomkn.server.plugins.configureCallLogging import org.tod87et.roomkn.server.plugins.configureRouting import org.tod87et.roomkn.server.plugins.configureSerialization @@ -18,5 +19,6 @@ fun Application.module() { configureRouting() configureSerialization() configureCleanup() + configureCallLogging() logger.info("RooMKN main module has been initialized") } diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AccountControllerImpl.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AccountControllerImpl.kt index 6fca17e6..b4f806b8 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AccountControllerImpl.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AccountControllerImpl.kt @@ -5,6 +5,9 @@ import com.auth0.jwt.JWTVerifier import com.auth0.jwt.algorithms.Algorithm import io.ktor.util.logging.Logger import io.ktor.utils.io.CancellationException +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.Date import kotlinx.coroutines.delay import kotlinx.datetime.toKotlinInstant import org.tod87et.roomkn.server.database.ConstraintViolationException @@ -13,9 +16,6 @@ import org.tod87et.roomkn.server.models.permissions.UserPermission import org.tod87et.roomkn.server.models.users.LoginUserInfo import org.tod87et.roomkn.server.models.users.RegistrationUserInfo import org.tod87et.roomkn.server.models.users.UnregisteredUserInfo -import java.security.MessageDigest -import java.security.SecureRandom -import java.util.Date class AccountControllerImpl( private val log: Logger, @@ -33,27 +33,12 @@ class AccountControllerImpl( private val signAlgorithm = Algorithm.HMAC256(config.secret) - override val jwtVerifier: JWTVerifier = JWT - .require(Algorithm.HMAC256(config.secret)) - .withAudience(config.audience) - .withIssuer(config.issuer) - .build() + override val jwtVerifier: JWTVerifier = + JWT.require(Algorithm.HMAC256(config.secret)).withAudience(config.audience).withIssuer(config.issuer).build() override fun authenticateUser(loginUserInfo: LoginUserInfo): Result { - val credentials = config.credentialsDatabase.getCredentialsInfoByUsername(loginUserInfo.username) - .getOrElse { ex -> - return when (ex) { - is MissingElementException -> { - Result.failure(NoSuchUserException(loginUserInfo.username, ex)) - } - - else -> { - Result.failure(ex) - } - } - } - val permissions = config.database.getUserPermissions(credentials.id) - .getOrElse { ex -> + val credentials = + config.credentialsDatabase.getCredentialsInfoByUsername(loginUserInfo.username).getOrElse { ex -> return when (ex) { is MissingElementException -> { Result.failure(NoSuchUserException(loginUserInfo.username, ex)) @@ -72,7 +57,7 @@ class AccountControllerImpl( val passwordHash = digest.digest() return if (passwordHash.contentEquals(credentials.passwordHash)) { log.debug("Authentication successful for `${loginUserInfo.username}`") - Result.success(AuthSession(createToken(credentials.id, permissions))) + Result.success(AuthSession(createToken(credentials.id))) } else { log.debug("Authentication failed for `${loginUserInfo.username}`") Result.failure(AuthFailedException("Wrong username or password")) @@ -83,11 +68,10 @@ class AccountControllerImpl( registerUser(userInfo, defaultPermissions) override fun validateSession(session: AuthSession): Result { - runCatching { jwtVerifier.verify(session.token) } - .getOrElse { - log.debug("Token verification failed", it) - return Result.success(false) - } + runCatching { jwtVerifier.verify(session.token) }.getOrElse { + log.debug("Token verification failed", it) + return Result.success(false) + } digest.update(session.token.encodeToByteArray()) return config.credentialsDatabase.checkTokenWasInvalidated(digest.digest()).map { !it } } @@ -95,8 +79,7 @@ class AccountControllerImpl( override fun invalidateSession(session: AuthSession): Result { digest.update(session.token.encodeToByteArray()) return config.credentialsDatabase.invalidateToken( - digest.digest(), - JWT.decode(session.token).expiresAtAsInstant.toKotlinInstant() + digest.digest(), JWT.decode(session.token).expiresAtAsInstant.toKotlinInstant() ) } @@ -113,8 +96,8 @@ class AccountControllerImpl( return } - val email = System.getenv(ENV_ROOMKN_SUPERUSER_EMAIL)?.takeUnless(String::isBlank) - ?: System.getenv(ENV_HOST)?.takeUnless(String::isEmpty)?.let { "admin@$it" } + val email = System.getenv(ENV_ROOMKN_SUPERUSER_EMAIL)?.takeUnless(String::isBlank) ?: System.getenv(ENV_HOST) + ?.takeUnless(String::isEmpty)?.let { "admin@$it" } if (email == null || config.credentialsDatabase.getCredentialsInfoByEmail(email).isSuccess) { log.warn( @@ -124,8 +107,7 @@ class AccountControllerImpl( } val res = registerUser( - UnregisteredUserInfo(username, email, password), - defaultAdminPermissions + UnregisteredUserInfo(username, email, password), defaultAdminPermissions ) if (res.isFailure) { @@ -156,10 +138,7 @@ class AccountControllerImpl( return when (ex) { is ConstraintViolationException -> { - if ( - ex.constraint == ConstraintViolationException.Constraint.USERNAME || - ex.constraint == ConstraintViolationException.Constraint.EMAIL - ) { + if (ex.constraint == ConstraintViolationException.Constraint.USERNAME || ex.constraint == ConstraintViolationException.Constraint.EMAIL) { Result.failure(RegistrationFailedException("User with such username already exists")) } else { Result.failure(ex) @@ -173,7 +152,10 @@ class AccountControllerImpl( } log.debug("User `${info.username}` has been registered") - return Result.success(AuthSession(createToken(info.id, defaultPermissions))) + + config.database.updateUserPermissions(info.id, permissions) + log.debug("Set user `{}` permissions to {}", info.username, permissions) + return Result.success(AuthSession(createToken(info.id))) } override suspend fun cleanerLoop() { @@ -190,12 +172,9 @@ class AccountControllerImpl( } } - private fun createToken(userId: Int, permissions: List): String { - return JWT.create() - .withAudience(config.audience) - .withIssuer(config.issuer) + private fun createToken(userId: Int): String { + return JWT.create().withAudience(config.audience).withIssuer(config.issuer) .withClaim(AuthSession.USER_ID_CLAIM_NAME, userId) - .withClaim(AuthSession.USER_PERMISSIONS_CLAIM_NAME, permissions.map { it.toString() }) .withExpiresAt(Date(System.currentTimeMillis() + config.tokenValidityPeriod.inWholeMilliseconds)) .sign(signAlgorithm) } diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthConfig.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthConfig.kt index 82510db9..9aae7281 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthConfig.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthConfig.kt @@ -1,16 +1,16 @@ package org.tod87et.roomkn.server.auth import io.ktor.server.config.ApplicationConfig -import org.tod87et.roomkn.server.database.CredentialsDatabase -import org.tod87et.roomkn.server.database.Database -import org.tod87et.roomkn.server.util.checkField import java.util.Base64 import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours +import org.tod87et.roomkn.server.database.CredentialsDatabase +import org.tod87et.roomkn.server.database.Database +import org.tod87et.roomkn.server.util.checkField class AuthConfig( - val issuer: String, + val issuer: String, val audience: String, val secret: ByteArray, val pepper: ByteArray, @@ -33,7 +33,7 @@ class AuthConfig( var saltSize: Int = DEFAULT_SALT_SIZE, var hashingAlgorithmId: String = DEFAULT_HASHING_ALGORITHM_ID, val cleanupInterval: Duration = DEFAULT_CLEANUP_INTERVAL, - ) { + ) { companion object { private val DEFAULT_TOKEN_VALIDITY_PERIOD: Duration = 30.days private const val DEFAULT_SALT_SIZE: Int = 32 @@ -56,7 +56,8 @@ class AuthConfig( fun secret(secret: ByteArray) = apply { this.secret = secret } - fun secret(base64EncodedSecret: String) = apply { this.secret = Base64.getDecoder().decode(base64EncodedSecret) } + fun secret(base64EncodedSecret: String) = + apply { this.secret = Base64.getDecoder().decode(base64EncodedSecret) } fun database(database: Database) = apply { this.database = database } @@ -65,7 +66,8 @@ class AuthConfig( fun pepper(pepper: ByteArray) = apply { this.pepper = pepper } - fun pepper(base64EncodedPepper: String) = apply { this.pepper = Base64.getDecoder().decode(base64EncodedPepper) } + fun pepper(base64EncodedPepper: String) = + apply { this.pepper = Base64.getDecoder().decode(base64EncodedPepper) } fun tokenValidityPeriod(tokenValidityPeriod: Duration) = apply { this.tokenValidityPeriod = tokenValidityPeriod } diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthSession.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthSession.kt index f4e4fe51..26440991 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthSession.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/auth/AuthSession.kt @@ -7,18 +7,10 @@ import org.tod87et.roomkn.server.models.permissions.UserPermission data class AuthSession(val token: String) : Principal { companion object { const val USER_ID_CLAIM_NAME: String = "userId" - const val USER_PERMISSIONS_CLAIM_NAME: String = "permissions" } } val AuthSession.userId: Int get() = JWT.decode(token).getClaim(AuthSession.USER_ID_CLAIM_NAME).asInt() ?: throw SecurityException("Cannot decode user id from token") -val AuthSession.permissions: List - get() = JWT.decode(token) - .getClaim(AuthSession.USER_PERMISSIONS_CLAIM_NAME) - .asList(String::class.java) - ?.mapNotNull { - runCatching { UserPermission.valueOf(it) }.getOrNull() - } ?: emptyList() diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/plugins/CallLogging.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/plugins/CallLogging.kt new file mode 100644 index 00000000..4320950c --- /dev/null +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/plugins/CallLogging.kt @@ -0,0 +1,21 @@ +package org.tod87et.roomkn.server.plugins + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.plugins.callloging.CallLogging +import io.ktor.server.request.httpMethod +import io.ktor.server.request.uri +import org.slf4j.event.Level + +fun Application.configureCallLogging() { + install(CallLogging) { + level = Level.INFO + format { call -> + val status = call.response.status() + val httpMethod = call.request.httpMethod.value + val userAgent = call.request.headers["User-Agent"] + val route = call.request.uri + "Status: $status, HTTP method: $httpMethod, User agent: $userAgent, Route: $route" + } + } +} diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/ReservationsRouting.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/ReservationsRouting.kt index 5e7b7e4c..2760e5f5 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/ReservationsRouting.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/ReservationsRouting.kt @@ -15,7 +15,6 @@ import io.ktor.server.routing.route import io.ktor.util.pipeline.PipelineContext import org.tod87et.roomkn.server.auth.AuthSession import org.tod87et.roomkn.server.auth.AuthenticationProvider -import org.tod87et.roomkn.server.auth.permissions import org.tod87et.roomkn.server.auth.userId import org.tod87et.roomkn.server.database.ConstraintViolationException import org.tod87et.roomkn.server.database.Database @@ -91,7 +90,7 @@ private fun Route.reservationDeleteRouting(database: Database) { .getOrElse { return@delete call.handleReservationException(it) } - call.requirePermissionOrSelf(reservation.userId) { return@delete call.onMissingPermission() } + call.requirePermissionOrSelf(reservation.userId, database) { return@delete call.onMissingPermission() } val result = database.deleteReservation(id) result @@ -121,12 +120,10 @@ private fun Route.reserveRouting(database: Database) { private inline fun ApplicationCall.requirePermissionOrSelf( self: Int, + database: Database, onPermissionMissing: () -> Nothing ) { - val session = principal() - if (session == null || session.userId != self && !session.permissions.contains(UserPermission.ReservationsAdmin)) { - onPermissionMissing() - } + requirePermissionOrSelfImpl(self, database, UserPermission.ReservationsAdmin, onPermissionMissing) } private suspend fun ApplicationCall.handleReservationException(ex: Throwable) { diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/UsersRouting.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/UsersRouting.kt index b46349ca..8f3c60c5 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/UsersRouting.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/UsersRouting.kt @@ -4,7 +4,6 @@ import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call import io.ktor.server.auth.authenticate -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.response.respondText import io.ktor.server.routing.Route @@ -12,11 +11,8 @@ import io.ktor.server.routing.delete import io.ktor.server.routing.get import io.ktor.server.routing.put import io.ktor.server.routing.route -import org.tod87et.roomkn.server.auth.AuthSession import org.tod87et.roomkn.server.auth.AuthenticationProvider import org.tod87et.roomkn.server.auth.NoSuchUserException -import org.tod87et.roomkn.server.auth.permissions -import org.tod87et.roomkn.server.auth.userId import org.tod87et.roomkn.server.database.Database import org.tod87et.roomkn.server.di.injectDatabase import org.tod87et.roomkn.server.models.permissions.UserPermission @@ -39,44 +35,36 @@ fun Route.usersRouting() { private fun Route.listUsers(database: Database) { get { - call.requirePermission { return@get call.onMissingPermission() } + call.requirePermission(database) { return@get call.onMissingPermission() } - database.getUsers() - .onSuccess { call.respond(it) } - .onFailure { call.handleException(it) } + database.getUsers().onSuccess { call.respond(it) }.onFailure { call.handleException(it) } } } private fun Route.deleteUser(database: Database) { delete("/{id}") { val id = call.parameters["id"]?.toInt() ?: return@delete call.onMissingId() - call.requirePermission { return@delete call.onMissingPermission() } + call.requirePermission(database) { return@delete call.onMissingPermission() } - database.deleteUser(id) - .onSuccess { call.respond("Ok") } - .onFailure { call.handleException(it) } + database.deleteUser(id).onSuccess { call.respond("Ok") }.onFailure { call.handleException(it) } } } private fun Route.updateUser(database: Database) { put("/{id}") { body: UpdateUserInfo -> val id = call.parameters["id"]?.toInt() ?: return@put call.onMissingId() - call.requirePermissionOrSelf(id) { return@put call.onMissingPermission() } + call.requirePermissionOrSelf(id, database) { return@put call.onMissingPermission() } - database.updateUserInfo(id, body) - .onSuccess { call.respond("Ok") } - .onFailure { call.handleException(it) } + database.updateUserInfo(id, body).onSuccess { call.respond("Ok") }.onFailure { call.handleException(it) } } } private fun Route.listUserPermissions(database: Database) { get("/{id}/permissions") { val id = call.parameters["id"]?.toInt() ?: return@get call.onMissingId() - call.requirePermissionOrSelf(id) { return@get call.onMissingPermission() } + call.requirePermissionOrSelf(id, database) { return@get call.onMissingPermission() } - database.getUserPermissions(id) - .onSuccess { call.respond(it) } - .onFailure { call.handleException(it) } + database.getUserPermissions(id).onSuccess { call.respond(it) }.onFailure { call.handleException(it) } } } @@ -84,19 +72,16 @@ private fun Route.getUser(database: Database) { get("/{id}") { val id = call.parameters["id"]?.toInt() ?: return@get call.onMissingId() - database.getUser(id) - .onSuccess { call.respond(it) } - .onFailure { call.handleException(it) } + database.getUser(id).onSuccess { call.respond(it) }.onFailure { call.handleException(it) } } } private fun Route.setUserPermissions(database: Database) { put("/{id}/permissions") { body: List -> val id = call.parameters["id"]?.toInt() ?: return@put call.onMissingId() - call.requirePermission { return@put call.onMissingPermission() } + call.requirePermission(database) { return@put call.onMissingPermission() } - database.updateUserPermissions(id, body) - .onSuccess { call.respondText("Ok") } + database.updateUserPermissions(id, body).onSuccess { call.respondText("Ok") } .onFailure { call.handleException(it) } } } @@ -114,24 +99,13 @@ private suspend fun ApplicationCall.handleException(ex: Throwable) { } private inline fun ApplicationCall.requirePermission( - onPermissionMissing: () -> Nothing + database: Database, onPermissionMissing: () -> Nothing ) { - requirePermissionOrSelfImpl(self = null, onPermissionMissing) + requirePermissionOrSelfImpl(self = null, database, UserPermission.UsersAdmin, onPermissionMissing) } private inline fun ApplicationCall.requirePermissionOrSelf( - self: Int, - onPermissionMissing: () -> Nothing + self: Int, database: Database, onPermissionMissing: () -> Nothing ) { - requirePermissionOrSelfImpl(self, onPermissionMissing) -} - -private inline fun ApplicationCall.requirePermissionOrSelfImpl( - self: Int?, - onPermissionMissing: () -> Nothing -) { - val session = principal() - if (session == null || session.userId != self && !session.permissions.contains(UserPermission.UsersAdmin)) { - onPermissionMissing() - } + requirePermissionOrSelfImpl(self, database, UserPermission.UsersAdmin, onPermissionMissing) } diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/defaultResponses.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/defaultResponses.kt index 3c5f411e..f77385f4 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/defaultResponses.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/defaultResponses.kt @@ -2,7 +2,13 @@ package org.tod87et.roomkn.server.routing import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall +import io.ktor.server.auth.principal import io.ktor.server.response.respondText +import org.jetbrains.exposed.sql.exposedLogger +import org.tod87et.roomkn.server.auth.AuthSession +import org.tod87et.roomkn.server.auth.userId +import org.tod87et.roomkn.server.database.Database +import org.tod87et.roomkn.server.models.permissions.UserPermission suspend fun ApplicationCall.onMissingPermission() { @@ -12,3 +18,30 @@ suspend fun ApplicationCall.onMissingPermission() { suspend fun ApplicationCall.onMissingId() { respondText("id should be int", status = HttpStatusCode.BadRequest) } + + +inline fun ApplicationCall.requirePermissionOrSelfImpl( + self: Int?, + database: Database, + requiredPermission: UserPermission, + onPermissionMissing: () -> Nothing +) { + val session = principal() + if (session == null) { + onPermissionMissing() + } else if (session.userId != self) { + database.getUserPermissions(session.userId) + .onFailure { onPermissionMissing() } + .onSuccess { permissions -> + if (!permissions.contains(requiredPermission)) { + exposedLogger.debug( + "User Id {} don't have {} - List of user permissions: {}", + session.userId, + requiredPermission, + permissions + ) + onPermissionMissing() + } + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/room.kt b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/room.kt index 510115fb..6c84a537 100644 --- a/server/src/main/kotlin/org/tod87et/roomkn/server/routing/room.kt +++ b/server/src/main/kotlin/org/tod87et/roomkn/server/routing/room.kt @@ -5,7 +5,6 @@ import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call import io.ktor.server.auth.authenticate -import io.ktor.server.auth.principal import io.ktor.server.response.respond import io.ktor.server.response.respondText import io.ktor.server.routing.Route @@ -14,16 +13,14 @@ import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.put import io.ktor.server.routing.route -import org.tod87et.roomkn.server.auth.AuthSession +import kotlin.math.min import org.tod87et.roomkn.server.auth.AuthenticationProvider -import org.tod87et.roomkn.server.auth.permissions import org.tod87et.roomkn.server.database.Database import org.tod87et.roomkn.server.database.MissingElementException import org.tod87et.roomkn.server.di.injectDatabase import org.tod87et.roomkn.server.models.permissions.UserPermission import org.tod87et.roomkn.server.models.rooms.NewRoomInfo import org.tod87et.roomkn.server.util.defaultExceptionHandler -import kotlin.math.min fun Route.roomsRouting() { val database by injectDatabase() @@ -47,7 +44,7 @@ private fun Route.roomReservationsRouting(database: Database) { private fun Route.createRoom(database: Database) { post { body: NewRoomInfo -> - call.requirePermission { return@post call.onMissingPermission() } + call.requirePermission(database) { return@post call.onMissingPermission() } database.createRoom(body) .onSuccess { @@ -62,7 +59,7 @@ private fun Route.createRoom(database: Database) { private fun Route.deleteRoom(database: Database) { delete("/{id}") { val id = call.parameters["id"]?.toInt() ?: return@delete call.onMissingId() - call.requirePermission { return@delete call.onMissingPermission() } + call.requirePermission(database) { return@delete call.onMissingPermission() } database.deleteRoom(id) .onSuccess { @@ -77,7 +74,7 @@ private fun Route.deleteRoom(database: Database) { private fun Route.updateRoom(database: Database) { put("/{id}") { body: NewRoomInfo -> val id = call.parameters["id"]?.toInt() ?: return@put call.onMissingId() - call.requirePermission { return@put call.onMissingPermission() } + call.requirePermission(database) { return@put call.onMissingPermission() } database.updateRoom(id, body) .onSuccess { @@ -101,13 +98,14 @@ private fun Route.rooms(database: Database) { val right = offset + limit if (limit < 0 || offset < 0 || offset > right) - return@get call.respondText ( + return@get call.respondText( "Incorrect limit or offset", status = HttpStatusCode.BadRequest ) result.onSuccess { - call.respond(HttpStatusCode.OK, + call.respond( + HttpStatusCode.OK, it.subList( min(offset, it.size), min(right, it.size) @@ -159,10 +157,8 @@ private suspend fun ApplicationCall.handleException(ex: Throwable) { } private inline fun ApplicationCall.requirePermission( + database: Database, onPermissionMissing: () -> Nothing ) { - val session = principal() - if (session == null || !session.permissions.contains(UserPermission.RoomsAdmin)) { - onPermissionMissing() - } + requirePermissionOrSelfImpl(null, database, UserPermission.RoomsAdmin, onPermissionMissing) } diff --git a/server/src/test/kotlin/org/tod87et/roomkn/server/UsersRoutesTests.kt b/server/src/test/kotlin/org/tod87et/roomkn/server/UsersRoutesTests.kt index eee1bef8..98f042d6 100644 --- a/server/src/test/kotlin/org/tod87et/roomkn/server/UsersRoutesTests.kt +++ b/server/src/test/kotlin/org/tod87et/roomkn/server/UsersRoutesTests.kt @@ -5,17 +5,19 @@ import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.put import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.contentType +import java.security.Permission +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.jetbrains.exposed.sql.exposedLogger import org.junit.jupiter.api.Test import org.tod87et.roomkn.server.models.permissions.UserPermission import org.tod87et.roomkn.server.models.users.ShortUserInfo import org.tod87et.roomkn.server.models.users.UpdateUserInfo import org.tod87et.roomkn.server.models.users.UserInfo -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.test.assertTrue class UsersRoutesTests { private val apiPath = "/api/v0" @@ -26,16 +28,21 @@ class UsersRoutesTests { @Test fun getUsers() = KtorTestEnv.testJsonApplication { client -> + var idRoot: Int with(KtorTestEnv) { - client.createAndAuthAdmin() + idRoot = client.createAndAuthAdmin("Root") } val id1 = KtorTestEnv.createUser("Bob") val id2 = KtorTestEnv.createUser("Alice") - val users = client.get(usersPath).body>() - - assertContains(users, ShortUserInfo(id1, "Bob")) - assertContains(users, ShortUserInfo(id2, "Alice")) + val response = client.get(usersPath) + assertEquals(HttpStatusCode.OK, response.status, "Message: ${response.bodyAsText()}\n") + val users = response.body>() + assertEquals( + setOf(ShortUserInfo(idRoot, "Root"), ShortUserInfo(id1, "Bob"), ShortUserInfo(id2, "Alice")), + users.toSet(), + "Expect all 3 users: Root, Bob, Alice" + ) } @Test @@ -136,7 +143,9 @@ class UsersRoutesTests { val permissions1 = listOf(UserPermission.UsersAdmin, UserPermission.GroupsAdmin) KtorTestEnv.database.updateUserPermissions(id, permissions1) - val permissions2 = client.get(userPermissionsPath(id)).body>() + val response = client.get(userPermissionsPath(id)) + assertEquals(HttpStatusCode.OK, response.status, "Message: ${response.bodyAsText()}") + val permissions2 = response.body>() assertEquals(permissions1, permissions2) } @@ -152,7 +161,7 @@ class UsersRoutesTests { contentType(ContentType.Application.Json) setBody(permissions) } - assertEquals(HttpStatusCode.OK, resp.status) + assertEquals(HttpStatusCode.OK, resp.status, "Message: ${resp.bodyAsText()}") assertEquals(permissions, KtorTestEnv.database.getUserPermissions(id).getOrThrow()) } }