Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: authorization todos in rsocket api #173

Merged
merged 1 commit into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.timemates.api.rsocket.auth

import io.rsocket.kotlin.payload.Payload
import io.timemates.backend.authorization.usecases.GetUserIdByAccessTokenUseCase
import io.timemates.backend.authorization.usecases.GetAuthorizationUseCase
import io.timemates.rsproto.metadata.ExtraMetadata
import io.timemates.rsproto.server.annotations.ExperimentalInterceptorsApi
import io.timemates.rsproto.server.interceptors.Interceptor
Expand All @@ -14,17 +14,17 @@ import kotlin.coroutines.CoroutineContext
*/
@OptIn(ExperimentalInterceptorsApi::class)
class AuthInterceptor(
private val getUserIdByAccessTokenUseCase: GetUserIdByAccessTokenUseCase,
private val getAuthorizationUseCase: GetAuthorizationUseCase,
) : Interceptor() {
data class Data(val accessHash: String?, val userIdProvider: GetUserIdByAccessTokenUseCase) : CoroutineContext.Element {
data class Data(val accessHash: String?, val authorizationProvider: GetAuthorizationUseCase) : CoroutineContext.Element {
override val key get() = Key

companion object Key : CoroutineContext.Key<Data>
}

override fun intercept(coroutineContext: CoroutineContext, incoming: Payload): CoroutineContext {
val accessHash = coroutineContext[ExtraMetadata]?.extra?.get("access_hash")?.decodeToString()
return coroutineContext + Data(accessHash, getUserIdByAccessTokenUseCase)
return coroutineContext + Data(accessHash, getAuthorizationUseCase)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class AuthorizationService(
}

override suspend fun terminateAuthorization(request: Empty): Empty {
removeAccessTokenUseCase.execute(AccessHash.createOrFail(Request.userAccessHash()))
removeAccessTokenUseCase.execute(AccessHash.createOrFail(Request.userAccessHash() ?: unauthorized()))
return Empty.Default
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ internal fun alreadyExists(): Nothing = throw ApiFailure.AlreadyExists
context(RSocketService)
internal fun noAccess(): Nothing = throw ApiFailure.NoAccess

context(RSocketService)
internal fun unauthorized(): Nothing = throw ApiFailure.Unauthorized

internal object ApiFailure {
/**
* Variable for custom RSocket error when the number of attempts exceeds a limit.
Expand Down Expand Up @@ -68,4 +71,10 @@ internal object ApiFailure {
*/
val NoAccess: RSocketError.Custom
get() = RSocketError.Custom(HttpStatusCode.Forbidden.value, "No access to given resource or operation.")

/**
* Represents unauthorized failure in requests.
*/
val Unauthorized: RSocketError.Custom
get() = RSocketError.Custom(HttpStatusCode.Unauthorized.value, "Unauthorized.")
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package io.timemates.api.rsocket.internal

import io.timemates.api.rsocket.auth.AuthInterceptor
import io.timemates.backend.authorization.types.value.AccessHash
import io.timemates.backend.authorization.usecases.GetAuthorizationUseCase
import io.timemates.backend.features.authorization.Authorized
import io.timemates.backend.features.authorization.AuthorizedContext
import io.timemates.backend.features.authorization.Scope
import io.timemates.backend.features.authorization.authorizationProvider
import io.timemates.backend.features.authorization.types.AuthorizedId
import io.timemates.rsproto.server.RSocketService
import kotlinx.coroutines.currentCoroutineContext

/**
* Executes the provided block of code within an authorized context.
Expand All @@ -11,6 +18,21 @@ import io.timemates.rsproto.server.RSocketService
* @return The result of executing the code block.
*/
context(RSocketService)
internal suspend inline fun <T : Scope, R> authorized(block: AuthorizedContext<T>.() -> R): R {
TODO()
internal suspend inline fun <reified T : Scope, R> authorized(
constraint: (List<Scope>) -> Boolean = { scopes -> scopes.any { it is T || it is Scope.All } },
block: AuthorizedContext<T>.() -> R,
): R {
val authInfo = currentCoroutineContext()[AuthInterceptor.Data] ?: unauthorized()
val accessHash = authInfo.accessHash ?: unauthorized()

return authorizationProvider(
provider = {
authInfo.authorizationProvider.execute(AccessHash.createOrFail(accessHash))
.let { (it as? GetAuthorizationUseCase.Result.Success)?.authorization ?: unauthorized() }
.takeIf { constraint(it.scopes) }
?.let { Authorized(AuthorizedId(it.userId.long), scopes = it.scopes) }
},
onFailure = { unauthorized() },
block = block,
)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.timemates.api.rsocket.internal

import io.rsocket.kotlin.RSocketError
import io.timemates.api.rsocket.auth.AuthInterceptor
import io.timemates.backend.validation.SafeConstructor
import io.timemates.backend.validation.ValidationFailureHandler
import io.timemates.rsproto.server.RSocketService
import kotlinx.coroutines.currentCoroutineContext

/**
* Used as a handler for validation inside RSocket requests.
Expand Down Expand Up @@ -35,5 +37,5 @@ internal fun <T, W> SafeConstructor<T, W>.createOrFail(value: W): T {

object Request {
context(RSocketService)
internal fun userAccessHash(): String = TODO()
internal suspend fun userAccessHash(): String? = currentCoroutineContext()[AuthInterceptor.Data]?.accessHash
}