Skip to content

Commit

Permalink
M8l2 auth (#36)
Browse files Browse the repository at this point in the history
* M8l2 auth

* M8l2 auth
  • Loading branch information
svok authored Jul 20, 2024
1 parent f465212 commit fe0441e
Show file tree
Hide file tree
Showing 66 changed files with 7,101 additions and 71 deletions.
3 changes: 2 additions & 1 deletion deploy/keycloak-tokens.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/bin/bash

KCHOST=http://localhost:8081
#KCHOST=http://localhost:32782
REALM=otus-marketplace
CLIENT_ID=otus-marketplace-service
UNAME=otus-test
Expand All @@ -13,7 +14,7 @@ PASSWORD=otus
# -d "grant_type=password" \
# "$KCHOST/auth/realms/$REALM/protocol/openid-connect/token" | jq -r '.access_token'`

ACCESS_TOKEN=`curl \
ACCESS_TOKEN=`curl -XPOST \
-d "client_id=$CLIENT_ID" \
-d "username=$UNAME" \
-d "password=$PASSWORD" \
Expand Down
3 changes: 3 additions & 0 deletions ok-marketplace-be/ok-marketplace-app-common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("build-kmp")
alias(libs.plugins.kotlinx.serialization)
}

kotlin {
Expand All @@ -9,6 +10,8 @@ kotlin {
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(libs.coroutines.core)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)

// transport models
implementation(project(":ok-marketplace-common"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package ru.otus.otuskotlin.marketplace.app.common

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.otus.otuskotlin.marketplace.common.models.MkplUserId
import ru.otus.otuskotlin.marketplace.common.permissions.MkplPrincipalModel
import ru.otus.otuskotlin.marketplace.common.permissions.MkplUserGroups
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

const val AUTH_HEADER: String = "x-jwt-payload"

@OptIn(ExperimentalEncodingApi::class)
fun String?.jwt2principal(): MkplPrincipalModel = this?.let { jwtHeader ->
val jwtJson = Base64.decode(jwtHeader).decodeToString()
println("JWT JSON PAYLOAD: $jwtJson")
val jwtObj = jsMapper.decodeFromString(JwtPayload.serializer(), jwtJson)
jwtObj.toPrincipal()
}
?: run {
println("No jwt found in headers")
MkplPrincipalModel.NONE
}

@OptIn(ExperimentalEncodingApi::class)
fun MkplPrincipalModel.createJwtTestHeader(): String {
val jwtObj = fromPrincipal()
val jwtJson = jsMapper.encodeToString(JwtPayload.serializer(), jwtObj)
return Base64.encode(jwtJson.encodeToByteArray())
}

private val jsMapper = Json {
ignoreUnknownKeys = true
}

@Serializable
private data class JwtPayload(
val aud: List<String>? = null,
val sub: String? = null,
@SerialName("family_name")
val familyName: String? = null,
@SerialName("given_name")
val givenName: String? = null,
@SerialName("middle_name")
val middleName: String? = null,
val groups: List<String>? = null,
)

private fun JwtPayload.toPrincipal(): MkplPrincipalModel = MkplPrincipalModel(
id = sub?.let { MkplUserId(it) } ?: MkplUserId.NONE,
fname = givenName ?: "",
mname = middleName ?: "",
lname = familyName ?: "",
groups = groups?.mapNotNull { it.toPrincipalGroup() }?.toSet() ?: emptySet(),
)

private fun MkplPrincipalModel.fromPrincipal(): JwtPayload = JwtPayload(
sub = id.takeIf { it != MkplUserId.NONE }?.asString(),
givenName = fname.takeIf { it.isNotBlank() },
middleName = mname.takeIf { it.isNotBlank() },
familyName = lname.takeIf { it.isNotBlank() },
groups = groups.mapNotNull { it.fromPrincipalGroup() }.toList().takeIf { it.isNotEmpty() } ?: emptyList(),
)

private fun String?.toPrincipalGroup(): MkplUserGroups? = when (this?.uppercase()) {
"USER" -> MkplUserGroups.USER
"ADMIN_AD" -> MkplUserGroups.ADMIN_AD
"MODERATOR_MP" -> MkplUserGroups.MODERATOR_MP
"TEST" -> MkplUserGroups.TEST
"BAN_AD" -> MkplUserGroups.BAN_AD
// TODO сделать обработку ошибок
else -> null
}

private fun MkplUserGroups?.fromPrincipalGroup(): String? = when (this) {
MkplUserGroups.USER -> "USER"
MkplUserGroups.ADMIN_AD -> "ADMIN_AD"
MkplUserGroups.MODERATOR_MP -> "MODERATOR_MP"
MkplUserGroups.TEST -> "TEST"
MkplUserGroups.BAN_AD -> "BAN_AD"
// TODO сделать обработку ошибок
else -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import ru.otus.otuskotlin.marketplace.api.v2.mappers.fromTransport
import ru.otus.otuskotlin.marketplace.api.v2.mappers.toTransportAd
import ru.otus.otuskotlin.marketplace.api.v2.models.IRequest
import ru.otus.otuskotlin.marketplace.api.v2.models.IResponse
import ru.otus.otuskotlin.marketplace.app.common.AUTH_HEADER
import ru.otus.otuskotlin.marketplace.app.common.controllerHelper
import ru.otus.otuskotlin.marketplace.app.common.jwt2principal
import ru.otus.otuskotlin.marketplace.app.ktor.MkplAppSettings
import kotlin.reflect.KClass

Expand All @@ -17,6 +19,7 @@ suspend inline fun <reified Q : IRequest, @Suppress("unused") reified R : IRespo
logId: String,
) = appSettings.controllerHelper(
{
principal = this@processV2.request.header(AUTH_HEADER).jwt2principal()
fromTransport(this@processV2.receive<Q>())
},
{ this@processV2.respond(toTransportAd() as R) },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ru.otus.otuskotlin.marketplace.app.ktor.auth

import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.testing.*
import ru.otus.otuskotlin.marketplace.api.v2.apiV2Mapper
import ru.otus.otuskotlin.marketplace.api.v2.models.*
import ru.otus.otuskotlin.marketplace.app.ktor.MkplAppSettings
import ru.otus.otuskotlin.marketplace.app.ktor.module
import ru.otus.otuskotlin.marketplace.common.MkplCorSettings
import ru.otus.otuskotlin.marketplace.repo.inmemory.AdRepoInMemory
import kotlin.test.Test
import kotlin.test.assertEquals

class AuthTest {
@Test
fun invalidAudience() = testApplication {
val client = createClient {
install(ContentNegotiation) {
json(apiV2Mapper)
}
}
application { module(MkplAppSettings(corSettings = MkplCorSettings(repoTest = AdRepoInMemory()))) }
val response = client.post("/v2/ad/create") {
addAuth(groups = emptyList())
contentType(ContentType.Application.Json)
setBody(
AdCreateRequest(
ad = AdCreateObject(
title = "xxsdgff",
description = "dfgdfg",
adType = DealSide.SUPPLY,
visibility = AdVisibility.PUBLIC,
),
debug = AdDebug(mode = AdRequestDebugMode.TEST)
)
)
}
val adObj = response.body<AdCreateResponse>()
assertEquals(200, response.status.value)
assertEquals(ResponseResult.ERROR, adObj.result)
assertEquals("access-create", adObj.errors?.first()?.code)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.otus.otuskotlin.marketplace.app.ktor.auth

import io.ktor.client.request.*
import ru.otus.otuskotlin.marketplace.app.common.AUTH_HEADER
import ru.otus.otuskotlin.marketplace.app.common.createJwtTestHeader
import ru.otus.otuskotlin.marketplace.common.models.MkplUserId
import ru.otus.otuskotlin.marketplace.common.permissions.MkplPrincipalModel
import ru.otus.otuskotlin.marketplace.common.permissions.MkplUserGroups
import ru.otus.otuskotlin.marketplace.stubs.MkplAdStubBolts.AD_DEMAND_BOLT1

fun HttpRequestBuilder.addAuth(principal: MkplPrincipalModel) {
header(AUTH_HEADER, principal.createJwtTestHeader())
}

fun HttpRequestBuilder.addAuth(
id: MkplUserId = AD_DEMAND_BOLT1.ownerId,
groups: Collection<MkplUserGroups> = listOf(MkplUserGroups.TEST, MkplUserGroups.USER),
) {
addAuth(MkplPrincipalModel(id, groups = groups.toSet()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import ru.otus.otuskotlin.marketplace.api.v2.mappers.toTransportRead
import ru.otus.otuskotlin.marketplace.api.v2.mappers.toTransportUpdate
import ru.otus.otuskotlin.marketplace.api.v2.models.*
import ru.otus.otuskotlin.marketplace.app.ktor.MkplAppSettings
import ru.otus.otuskotlin.marketplace.app.ktor.auth.addAuth
import ru.otus.otuskotlin.marketplace.app.ktor.module
import ru.otus.otuskotlin.marketplace.common.models.MkplAdId
import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock
import ru.otus.otuskotlin.marketplace.common.models.MkplDealSide
import ru.otus.otuskotlin.marketplace.common.permissions.MkplUserGroups
import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -167,6 +169,7 @@ abstract class V2AdRepoBaseTest {
val response = client.post("/v2/ad/$func") {
contentType(ContentType.Application.Json)
header("X-Trace-Id", "12345")
addAuth(groups = listOf(MkplUserGroups.USER))
setBody(request)
}
function(response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import ru.otus.otuskotlin.marketplace.api.v1.models.IRequest
import ru.otus.otuskotlin.marketplace.api.v1.models.IResponse
import ru.otus.otuskotlin.marketplace.app.common.AUTH_HEADER
import ru.otus.otuskotlin.marketplace.app.common.controllerHelper
import ru.otus.otuskotlin.marketplace.app.common.jwt2principal
import ru.otus.otuskotlin.marketplace.app.ktor.MkplAppSettings
import ru.otus.otuskotlin.marketplace.mappers.v1.fromTransport
import ru.otus.otuskotlin.marketplace.mappers.v1.toTransportAd
Expand All @@ -17,6 +19,7 @@ suspend inline fun <reified Q : IRequest, @Suppress("unused") reified R : IRespo
logId: String,
) = appSettings.controllerHelper(
{
principal = this@processV1.request.header(AUTH_HEADER).jwt2principal()
fromTransport(receive<Q>())
},
{ respond(toTransportAd()) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import io.ktor.serialization.jackson.*
import io.ktor.server.testing.*
import ru.otus.otuskotlin.marketplace.api.v1.models.*
import ru.otus.otuskotlin.marketplace.app.ktor.MkplAppSettings
import ru.otus.otuskotlin.marketplace.app.ktor.auth.addAuth
import ru.otus.otuskotlin.marketplace.app.ktor.moduleJvm
import ru.otus.otuskotlin.marketplace.common.models.MkplAdId
import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock
import ru.otus.otuskotlin.marketplace.common.models.MkplDealSide
import ru.otus.otuskotlin.marketplace.common.permissions.MkplUserGroups
import ru.otus.otuskotlin.marketplace.mappers.v1.toTransportCreate
import ru.otus.otuskotlin.marketplace.mappers.v1.toTransportDelete
import ru.otus.otuskotlin.marketplace.mappers.v1.toTransportRead
Expand Down Expand Up @@ -164,6 +166,7 @@ abstract class V1AdRepoBaseTest {
val response = client.post("/v1/ad/$func") {
contentType(ContentType.Application.Json)
header("X-Trace-Id", "12345")
addAuth(groups = listOf(MkplUserGroups.USER))
setBody(request)
}
function(response)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package ru.otus.otuskotlin.markeplace.app.spring.controllers

import org.springframework.http.HttpHeaders
import org.springframework.web.bind.annotation.*
import ru.otus.otuskotlin.markeplace.app.spring.base.MkplAppSettings
import ru.otus.otuskotlin.marketplace.api.v1.models.*
import ru.otus.otuskotlin.marketplace.app.common.AUTH_HEADER
import ru.otus.otuskotlin.marketplace.app.common.controllerHelper
import ru.otus.otuskotlin.marketplace.app.common.jwt2principal
import ru.otus.otuskotlin.marketplace.mappers.v1.fromTransport
import ru.otus.otuskotlin.marketplace.mappers.v1.toTransportAd
import kotlin.reflect.KClass
Expand All @@ -16,37 +19,39 @@ class AdControllerV1Fine(
) {

@PostMapping("create")
suspend fun create(@RequestBody request: AdCreateRequest): AdCreateResponse =
process(appSettings, request = request, this::class, "create")
suspend fun create(@RequestBody request: AdCreateRequest, @RequestHeader headers: HttpHeaders): AdCreateResponse =
process(appSettings, request = request, headers = headers, this::class, "create")

@PostMapping("read")
suspend fun read(@RequestBody request: AdReadRequest): AdReadResponse =
process(appSettings, request = request, this::class, "read")
suspend fun read(@RequestBody request: AdReadRequest, @RequestHeader headers: HttpHeaders): AdReadResponse =
process(appSettings, request = request, headers = headers, this::class, "read")

@RequestMapping("update", method = [RequestMethod.POST])
suspend fun update(@RequestBody request: AdUpdateRequest): AdUpdateResponse =
process(appSettings, request = request, this::class, "update")
suspend fun update(@RequestBody request: AdUpdateRequest, @RequestHeader headers: HttpHeaders): AdUpdateResponse =
process(appSettings, request = request, headers = headers, this::class, "update")

@PostMapping("delete")
suspend fun delete(@RequestBody request: AdDeleteRequest): AdDeleteResponse =
process(appSettings, request = request, this::class, "delete")
suspend fun delete(@RequestBody request: AdDeleteRequest, @RequestHeader headers: HttpHeaders): AdDeleteResponse =
process(appSettings, request = request, headers = headers, this::class, "delete")

@PostMapping("search")
suspend fun search(@RequestBody request: AdSearchRequest): AdSearchResponse =
process(appSettings, request = request, this::class, "search")
suspend fun search(@RequestBody request: AdSearchRequest, @RequestHeader headers: HttpHeaders): AdSearchResponse =
process(appSettings, request = request, headers = headers, this::class, "search")

@PostMapping("offers")
suspend fun offers(@RequestBody request: AdOffersRequest): AdOffersResponse =
process(appSettings, request = request, this::class, "offers")
suspend fun offers(@RequestBody request: AdOffersRequest, @RequestHeader headers: HttpHeaders): AdOffersResponse =
process(appSettings, request = request, headers = headers, this::class, "offers")

companion object {
suspend inline fun <reified Q : IRequest, reified R : IResponse> process(
appSettings: MkplAppSettings,
request: Q,
headers: HttpHeaders,
clazz: KClass<*>,
logId: String,
): R = appSettings.controllerHelper(
{
principal = headers[AUTH_HEADER]?.first().jwt2principal()
fromTransport(request)
},
{ toTransportAd() as R },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package ru.otus.otuskotlin.markeplace.app.spring.controllers

import org.springframework.http.HttpHeaders
import org.springframework.web.bind.annotation.*
import ru.otus.otuskotlin.markeplace.app.spring.base.MkplAppSettings
import ru.otus.otuskotlin.marketplace.api.v2.mappers.*
import ru.otus.otuskotlin.marketplace.api.v2.models.*
import ru.otus.otuskotlin.marketplace.app.common.AUTH_HEADER
import ru.otus.otuskotlin.marketplace.app.common.controllerHelper
import ru.otus.otuskotlin.marketplace.app.common.jwt2principal
import kotlin.reflect.KClass

@Suppress("unused")
Expand All @@ -13,37 +16,39 @@ import kotlin.reflect.KClass
class AdControllerV2Fine(private val appSettings: MkplAppSettings) {

@PostMapping("create")
suspend fun create(@RequestBody request: AdCreateRequest): AdCreateResponse =
process(appSettings, request = request, this::class, "create")
suspend fun create(@RequestBody request: AdCreateRequest, @RequestHeader headers: HttpHeaders): AdCreateResponse =
process(appSettings, request = request, headers = headers, this::class, "create")

@PostMapping("read")
suspend fun read(@RequestBody request: AdReadRequest): AdReadResponse =
process(appSettings, request = request, this::class, "read")
suspend fun read(@RequestBody request: AdReadRequest, @RequestHeader headers: HttpHeaders): AdReadResponse =
process(appSettings, request = request, headers = headers, this::class, "read")

@RequestMapping("update", method = [RequestMethod.POST])
suspend fun update(@RequestBody request: AdUpdateRequest): AdUpdateResponse =
process(appSettings, request = request, this::class, "update")
suspend fun update(@RequestBody request: AdUpdateRequest, @RequestHeader headers: HttpHeaders): AdUpdateResponse =
process(appSettings, request = request, headers = headers, this::class, "update")

@PostMapping("delete")
suspend fun delete(@RequestBody request: AdDeleteRequest): AdDeleteResponse =
process(appSettings, request = request, this::class, "delete")
suspend fun delete(@RequestBody request: AdDeleteRequest, @RequestHeader headers: HttpHeaders): AdDeleteResponse =
process(appSettings, request = request, headers = headers, this::class, "delete")

@PostMapping("search")
suspend fun search(@RequestBody request: AdSearchRequest): AdSearchResponse =
process(appSettings, request = request, this::class, "search")
suspend fun search(@RequestBody request: AdSearchRequest, @RequestHeader headers: HttpHeaders): AdSearchResponse =
process(appSettings, request = request, headers = headers, this::class, "search")

@PostMapping("offers")
suspend fun offers(@RequestBody request: AdOffersRequest): AdOffersResponse =
process(appSettings, request = request, this::class, "offers")
suspend fun offers(@RequestBody request: AdOffersRequest, @RequestHeader headers: HttpHeaders): AdOffersResponse =
process(appSettings, request = request, headers = headers, this::class, "offers")

companion object {
suspend inline fun <reified Q : IRequest, reified R : IResponse> process(
appSettings: MkplAppSettings,
request: Q,
headers: HttpHeaders,
clazz: KClass<*>,
logId: String,
): R = appSettings.controllerHelper(
{
principal = headers[AUTH_HEADER]?.first().jwt2principal()
fromTransport(request)
},
{ toTransportAd() as R },
Expand Down
Loading

0 comments on commit fe0441e

Please sign in to comment.