auth groundwork for xs support
parent
3036ba243c
commit
92b53529f6
|
|
@ -1,4 +1,6 @@
|
||||||
|
import gradle.kotlin.dsl.accessors._6ebf0b5d05ec1eff67605a516c5db18b.implementation
|
||||||
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.api
|
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.api
|
||||||
|
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.testImplementation
|
||||||
import org.gradle.api.plugins.jvm.JvmTestSuite
|
import org.gradle.api.plugins.jvm.JvmTestSuite
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
|
@ -20,6 +22,8 @@ dependencies {
|
||||||
|
|
||||||
api("io.github.oshai:kotlin-logging-jvm:5.1.0")
|
api("io.github.oshai:kotlin-logging-jvm:5.1.0")
|
||||||
testImplementation("io.insert-koin:koin-test:$koin_version")
|
testImplementation("io.insert-koin:koin-test:$koin_version")
|
||||||
|
testImplementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
|
testImplementation("com.willowtreeapps.assertk:assertk:0.28.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,6 @@ dependencies {
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||||
|
implementation("com.akuleshov7:ktoml-core:0.5.1")
|
||||||
|
implementation("com.akuleshov7:ktoml-file:0.5.1")
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
package de.itkl.httpClient
|
package de.itkl.httpClient
|
||||||
|
|
||||||
|
import de.itkl.httpClient.implementation.SmartCloudAuthStrategy
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.client.plugins.api.*
|
import io.ktor.client.plugins.api.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
import io.ktor.util.*
|
import io.ktor.util.*
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
private val Log = KotlinLogging.logger { }
|
private val Log = KotlinLogging.logger { }
|
||||||
val smartCloudAuthPlugin = createClientPlugin("SmartCloudAuth") {
|
val smartCloudAuthPlugin = createClientPlugin("SmartCloudAuth") {
|
||||||
|
val smartCloudAuthStrategy = SmartCloudAuthStrategy()
|
||||||
on(Send) { request ->
|
on(Send) { request ->
|
||||||
request.attributes.getOrNull(AttributeKey<String>("user"))?.let { username ->
|
val token = smartCloudAuthStrategy.login(client, request) ?: error("login failed")
|
||||||
Log.info { "Provide bearer token for $username" }
|
request.headers {
|
||||||
TODO("retrieve user token ")
|
set(HttpHeaders.Authorization, "Bearer ${token.toBearer()}")
|
||||||
}
|
}
|
||||||
TODO()
|
proceed(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
|
||||||
|
interface AuthStrategy {
|
||||||
|
suspend fun login(
|
||||||
|
httpClient: HttpClient,
|
||||||
|
request: HttpRequestBuilder
|
||||||
|
): AuthenticationToken?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
sealed class AuthenticationToken {
|
||||||
|
abstract val expires: Expires
|
||||||
|
abstract fun toBearer(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BearerToken(val token: String, val validUntil: Instant) : AuthenticationToken() {
|
||||||
|
override val expires: Expires
|
||||||
|
get() = Expires.ExpiresAt(validUntil)
|
||||||
|
|
||||||
|
override fun toBearer(): String {
|
||||||
|
return "Bearer $token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sealed class Expires {
|
||||||
|
data object Never : Expires()
|
||||||
|
data class ExpiresAt(val instant: Instant) : Expires()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
|
typealias UrlMatcher = (Url) -> Boolean
|
||||||
|
class Authenticator : KoinComponent {
|
||||||
|
fun addStrategy(strategy: AuthStrategy, urlMatcher: UrlMatcher) {}
|
||||||
|
|
||||||
|
fun requiresAuthentication(url: Url): Boolean {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
suspend fun authenticate(url: Url, username: String): AuthenticationToken? {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
sealed class Credentials {
|
||||||
|
@Serializable
|
||||||
|
data class LoginAndPassword(val login: String, val password: String) : Credentials()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
interface CredentialsProvider {
|
||||||
|
suspend fun lookupByUsername(username: String): Credentials?
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,57 @@
|
||||||
package de.itkl.httpClient.clients
|
package de.itkl.httpClient.clients
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.request.forms.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.client.utils.EmptyContent.contentType
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.http.content.*
|
||||||
|
import io.ktor.util.cio.*
|
||||||
|
import io.ktor.utils.io.*
|
||||||
|
import io.ktor.utils.io.streams.*
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.isRegularFile
|
||||||
|
import kotlin.io.path.name
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
|
|
||||||
class XsClient : KoinComponent {
|
class XsClient : KoinComponent {
|
||||||
private val httpClient by inject<HttpClient>()
|
private val httpClient by inject<HttpClient>()
|
||||||
|
|
||||||
|
suspend fun analyse(image: Path) {
|
||||||
|
Log.info { "Starting analysis for image at path: $image" }
|
||||||
|
|
||||||
|
check(image.isRegularFile()) {
|
||||||
|
"The provided path $image is not a file"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info { "Submitting form with binary data to http://localhost:8080/api/v1/analyse-async" }
|
||||||
|
val response = httpClient.submitFormWithBinaryData(
|
||||||
|
url = "http://localhost:8080/api/v1/analyse-async",
|
||||||
|
formData {
|
||||||
|
append(
|
||||||
|
"image",
|
||||||
|
image.toChannelProvider(),
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, ContentType.defaultForFile(image))
|
||||||
|
append(HttpHeaders.ContentDisposition, """filename="${image.name}"""")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val responseText = response.bodyAsText()
|
||||||
|
Log.info { "Form submitted successfully: ${response.status}: $responseText" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Path.toChannelProvider(): ChannelProvider {
|
||||||
|
val file = toFile()
|
||||||
|
return ChannelProvider(file.length()) { file.readChannel() }
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
package de.itkl.httpClient
|
package de.itkl.httpClient
|
||||||
|
|
||||||
|
import de.itkl.httpClient.auth.CredentialsProvider
|
||||||
import de.itkl.httpClient.clients.MsOcr
|
import de.itkl.httpClient.clients.MsOcr
|
||||||
|
import de.itkl.httpClient.clients.XsClient
|
||||||
|
import de.itkl.httpClient.implementation.StaticCredentialsProvider
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
val httpClientModule = module {
|
val httpClientModule = module {
|
||||||
single<HttpClient> { createHttpClient() }
|
single<HttpClient> { createHttpClient() }
|
||||||
single<MsOcr> { MsOcr() }
|
single<MsOcr> { MsOcr() }
|
||||||
|
single<CredentialsProvider> {
|
||||||
|
val homeDirectory = System.getProperty("user.home")
|
||||||
|
val credentialsFilePath = Paths.get(homeDirectory, ".auth.toml")
|
||||||
|
StaticCredentialsProvider.load(credentialsFilePath)
|
||||||
|
}
|
||||||
|
single<XsClient> { XsClient() }
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package de.itkl.httpClient.implementation
|
||||||
|
|
||||||
|
import de.itkl.httpClient.auth.*
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.util.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
class SmartCloudAuthStrategy : AuthStrategy, KoinComponent {
|
||||||
|
private val credentialsProvider: CredentialsProvider by inject()
|
||||||
|
override suspend fun login(httpClient: HttpClient, request: HttpRequestBuilder): AuthenticationToken? {
|
||||||
|
Log.debug { "Attempting login..." }
|
||||||
|
val user = request.attributes.getOrNull(AttributeKey<String>("username")) ?: error("No username is specified for this request: $request")
|
||||||
|
val credentials: Credentials = credentialsProvider.lookupByUsername(user) ?: error("No credentials found for user: $user")
|
||||||
|
val loginAndPassword: Credentials.LoginAndPassword = credentials as? Credentials.LoginAndPassword ?: error("Only username and password is supported by smartcloud auth login")
|
||||||
|
|
||||||
|
Log.debug { "User: $user, using smartcloud auth login" }
|
||||||
|
|
||||||
|
val response = httpClient.post {
|
||||||
|
url("https://api.internal.insiders.cloud/1/rest/accounts/authentication/requesttoken")
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(loginAndPassword)
|
||||||
|
}
|
||||||
|
check(response.status.isSuccess()) {
|
||||||
|
"could not login into smart cloud: ${response.bodyAsText()}"
|
||||||
|
}
|
||||||
|
val token = response.body<BearerToken>()
|
||||||
|
Log.info { "Login successful. Valid until: ${token.validUntil}" }
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package de.itkl.httpClient.implementation
|
||||||
|
|
||||||
|
import com.akuleshov7.ktoml.Toml
|
||||||
|
import de.itkl.httpClient.auth.Credentials
|
||||||
|
import de.itkl.httpClient.auth.CredentialsProvider
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
class StaticCredentialsProvider(private val credentials: Map<String, Credentials.LoginAndPassword>) : CredentialsProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun load(path: Path): StaticCredentialsProvider {
|
||||||
|
Log.info { "Loading credentials from path: $path" }
|
||||||
|
val content = Files.readString(path)
|
||||||
|
val credentials = Toml.decodeFromString<CredentialsTable>(content).toCredentialsMap()
|
||||||
|
Log.info { "Credentials loaded successfully" }
|
||||||
|
return StaticCredentialsProvider(credentials)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun lookupByUsername(username: String): Credentials? {
|
||||||
|
Log.info { "Looking up credentials by username: $username" }
|
||||||
|
val credentialsEntry = credentials[username]
|
||||||
|
if (credentialsEntry != null) {
|
||||||
|
Log.info { "Credentials found for username: $username" }
|
||||||
|
return credentialsEntry
|
||||||
|
}
|
||||||
|
Log.info { "No credentials found for username: $username" }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CredentialsTable(
|
||||||
|
val credentials: Map<String, Map<String, String>>
|
||||||
|
) {
|
||||||
|
fun toCredentialsMap(): Map<String, Credentials.LoginAndPassword> {
|
||||||
|
return credentials.mapValues {
|
||||||
|
val login = it.value["Login"] ?: throw IllegalArgumentException("Missing login")
|
||||||
|
val password = it.value["Password"] ?: throw IllegalArgumentException("Missing password")
|
||||||
|
Credentials.LoginAndPassword(login, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package de.itkl.httpClient.implementation
|
||||||
|
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isDataClassEqualTo
|
||||||
|
import assertk.assertions.isNotNull
|
||||||
|
import de.itkl.httpClient.auth.Credentials
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
|
||||||
|
class StaticCredentialsProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can load credentials file`() = runBlocking {
|
||||||
|
val target = StaticCredentialsProvider.load(Paths.get("../../assets/credentials.toml"))
|
||||||
|
val credentials = target.lookupByUsername("test user")
|
||||||
|
assertThat(credentials)
|
||||||
|
.isNotNull()
|
||||||
|
.isDataClassEqualTo(Credentials.LoginAndPassword("TestiTest", "Secret"))
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue