Skip to content

Commit

Permalink
Authentication through Envoy bound to Keycloak (#15)
Browse files Browse the repository at this point in the history
* Experiments with ydb native

* Fix building

* Envoy authentication with Keycloak
  • Loading branch information
svok authored Feb 4, 2024
1 parent 28cc2f7 commit d1b4577
Show file tree
Hide file tree
Showing 54 changed files with 9,873 additions and 142 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
/kotlin-js-store/
**/logs/
!**/src/logs/

**/.vscode/

**/Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ package com.crowdproj.ad.api.v1

import kotlinx.serialization.json.Json

val cwpAdApiV1Json = Json {
ignoreUnknownKeys = true
}
val cwpAdApiV1Json = Json
1 change: 1 addition & 0 deletions crowdproj-ad-back/crowdproj-ad-app-ktor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ kotlin {
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.serialization)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.server.double.receive)

implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.server.auto.head.response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package com.crowdproj.ad.app.helpers

import com.crowdproj.ad.api.v1.mappers.fromApi
import com.crowdproj.ad.api.v1.mappers.toApi
import com.crowdproj.ad.api.v1.models.IRequestAd
import com.crowdproj.ad.api.v1.models.IResponseAd
import com.crowdproj.ad.api.v1.models.*
import com.crowdproj.ad.app.configs.CwpAdAppSettings
import com.crowdproj.ad.common.CwpAdContext
import com.crowdproj.ad.common.helpers.asCwpAdError
import com.crowdproj.ad.common.helpers.fail
import com.crowdproj.ad.common.models.CwpAdCommand
import com.crowdproj.ad.common.models.CwpAdRequestId
import io.ktor.http.*
import io.ktor.server.application.*
Expand All @@ -34,9 +34,12 @@ suspend inline fun <reified Rq : IRequestAd, reified Rs : IResponseAd> Applicati
try {
logger.info("Started $endpoint request $requestId")
val reqData = this.receive<Rq>()
val headers = this.request.headers
logger.info("HEADERS: \n${headers.entries().joinToString("\n") { "${it.key}=${it.value}" }}")
ctx.fromApi(reqData)
ctx.principal = this.request.jwt2principal()
appConfig.processor.exec(ctx)
respond<Rs>(ctx.toApi() as Rs)
respond<IResponseAd>(ctx.toApi() as Rs)
logger.info("Finished $endpoint request $requestId")
} catch (e: BadRequestException) {
logger.error(
Expand All @@ -50,11 +53,27 @@ suspend inline fun <reified Rq : IRequestAd, reified Rs : IResponseAd> Applicati
)
)
appConfig.processor.exec(ctx)
respond<Rs>(ctx.toApi() as Rs)
respond<IResponseAd>(ctx.toApi() as Rs)
} catch (e: Throwable) {
ctx.command = when(Rq::class) {
AdCreateRequest::class -> CwpAdCommand.CREATE
AdReadRequest::class -> CwpAdCommand.READ
AdUpdateRequest::class -> CwpAdCommand.UPDATE
AdDeleteRequest::class -> CwpAdCommand.DELETE
AdSearchRequest::class -> CwpAdCommand.SEARCH
AdOffersRequest::class -> CwpAdCommand.OFFERS
else -> CwpAdCommand.NONE
}
logger.error(
"Fail to handle $endpoint request $requestId with exception", e
)
// logger.error("Error request: ${receiveText()}")
try {
println("ERROR REQUEST: ${receiveText()}")
} catch (e: Throwable) {
println("RECEIVE TEXT is not WORKING!")
}

ctx.fail(
e.asCwpAdError(
message = "Unknown error. We have been informed and dealing with the problem."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.crowdproj.ad.app.helpers

import com.crowdproj.ad.common.models.CwpAdUserId
import com.crowdproj.ad.common.permissions.CwpAdPrincipalModel
import com.crowdproj.ad.common.permissions.CwpAdUserGroups
import io.ktor.server.request.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

@OptIn(ExperimentalEncodingApi::class)
fun ApplicationRequest.jwt2principal(): CwpAdPrincipalModel = this.header("x-jwt-payload")
?.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")
CwpAdPrincipalModel.NONE
}

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(): CwpAdPrincipalModel = CwpAdPrincipalModel(
id = sub?.let { CwpAdUserId(it) } ?: CwpAdUserId.NONE,
fname = givenName ?: "",
mname = middleName ?: "",
lname = familyName ?: "",
groups = groups?.mapNotNull { it.toPrincipalGroup() }?.toSet() ?: emptySet(),
)

private fun String?.toPrincipalGroup(): CwpAdUserGroups? = when (this?.uppercase()) {
"USER" -> CwpAdUserGroups.USER
"ADMIN_AD" -> CwpAdUserGroups.ADMIN_AD
"MODERATOR_MP" -> CwpAdUserGroups.MODERATOR_MP
"TEST" -> CwpAdUserGroups.TEST
"BAN_AD" -> CwpAdUserGroups.BAN_AD
// TODO сделать обработку ошибок
else -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.*
import io.ktor.server.plugins.callid.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.doublereceive.*
import io.ktor.server.routing.*

fun Application.initRest(appConfig: CwpAdAppSettings) {
install(Routing)
install(IgnoreTrailingSlash)
install(AutoHeadResponse)
install(DoubleReceive)
install(CallId) {
retrieveFromHeader("X-Request-ID")
val rx = Regex("^[0-9a-zA-Z-]{3,100}\$")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.crowdproj.ad.app

import com.crowdproj.ad.api.v1.models.*
import kotlinx.serialization.json.Json
import kotlin.test.Test

class AdRequestSerializationTest {
@Test
fun test() {
val req = AdCreateRequest(
ad = AdCreateObject(
title = "my title",
description = "my description",
adType = DealSide.DEMAND,
visibility = AdVisibility.PUBLIC,
productId = "23423423",
),
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.SUCCESS,
)
)
val json = Json.encodeToString(AdCreateRequest.serializer(), req)
println("REQUEST: $json")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.crowdproj.ad.app.stub

import com.crowdproj.ad.api.v1.cwpAdApiV1Json
import com.crowdproj.ad.api.v1.models.*
import com.crowdproj.ad.app.module
import io.ktor.client.*
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 kotlin.test.Test
import kotlin.test.assertEquals

class V1AdStubApiTest {

@Test
fun create() = v2TestApplication { client ->
val response = client.post("/v1/create") {
val requestObj = AdCreateRequest(
ad = AdCreateObject(
title = "Болт",
description = "КРУТЕЙШИЙ",
adType = DealSide.DEMAND,
visibility = AdVisibility.PUBLIC,
),
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.SUCCESS
)
)
contentType(ContentType.Application.Json)
header("X-Request-ID", "12345")
setBody(requestObj)
}
val responseObj = response.body<IResponseAd>() as AdCreateResponse
assertEquals(200, response.status.value)
assertEquals("666", responseObj.ad?.id)
}

@Test
fun read() = v2TestApplication { client ->
val response = client.post("/v1/read") {
val requestObj = AdReadRequest(
ad = AdReadObject("666"),
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.SUCCESS
)
)
contentType(ContentType.Application.Json)
header("X-Request-ID", "12345")
setBody(requestObj)
}
val responseObj = response.body<IResponseAd>() as AdReadResponse
assertEquals(200, response.status.value)
assertEquals("666", responseObj.ad?.id)
}

@Test
fun update() = v2TestApplication { client ->
val response = client.post("/v1/update") {
val requestObj = AdUpdateRequest(
ad = AdUpdateObject(
id = "666",
title = "Болт",
description = "КРУТЕЙШИЙ",
adType = DealSide.DEMAND,
visibility = AdVisibility.PUBLIC,
),
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.SUCCESS
)
)
contentType(ContentType.Application.Json)
header("X-Request-ID", "12345")
setBody(requestObj)
}
val responseObj = response.body<IResponseAd>() as AdUpdateResponse
assertEquals(200, response.status.value)
assertEquals("666", responseObj.ad?.id)
}

@Test
fun delete() = v2TestApplication { client ->
val response = client.post("/v1/delete") {
val requestObj = AdDeleteRequest(
ad = AdDeleteObject(
id = "666",
lock = "123"
),
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.SUCCESS
)
)
contentType(ContentType.Application.Json)
header("X-Request-ID", "12345")
setBody(requestObj)
}
val responseObj = response.body<IResponseAd>() as AdDeleteResponse
assertEquals(200, response.status.value)
assertEquals("666", responseObj.ad?.id)
}

@Test
fun search() = v2TestApplication { client ->
val response = client.post("/v1/search") {
val requestObj = AdSearchRequest(
adFilter = AdSearchFilter(),
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.SUCCESS
)
)
contentType(ContentType.Application.Json)
header("X-Request-ID", "12345")
setBody(requestObj)
}
val responseObj = response.body<IResponseAd>() as AdSearchResponse
assertEquals(200, response.status.value)
assertEquals("d-666-01", responseObj.ads?.first()?.id)
}

@Test
fun offers() = v2TestApplication { client ->
val response = client.post("/v1/offers") {
val requestObj = AdOffersRequest(
ad = AdReadObject(
id = "666"
),
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.SUCCESS
)
)
contentType(ContentType.Application.Json)
header("X-Request-ID", "12345")
setBody(requestObj)
}
val responseObj = response.body<IResponseAd>() as AdOffersResponse
assertEquals(200, response.status.value)
assertEquals("s-666-01", responseObj.ads?.first()?.id)
}

private fun v2TestApplication(function: suspend (HttpClient) -> Unit): Unit = testApplication {
application { module() }
val client = createClient {
install(ContentNegotiation) {
json(cwpAdApiV1Json)
}
}
function(client)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ actual fun main(args: Array<String>) {
config = conf
println("File read")

module {
module()
}

connector {
port = conf.port
host = conf.host
port = conf.port
}
module(Application::module)

println("Starting")
}).apply {
addShutdownHook {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.crowdproj.ad.common
import com.crowdproj.ad.common.config.CwpAdCorSettings
import kotlinx.datetime.Instant
import com.crowdproj.ad.common.models.*
import com.crowdproj.ad.common.permissions.CwpAdPrincipalModel
import com.crowdproj.ad.common.permissions.CwpAdUserPermissions
import com.crowdproj.ad.common.stubs.CwpAdStubs
import com.crowdproj.ad.common.repo.IAdRepository

Expand All @@ -17,6 +19,10 @@ data class CwpAdContext(
var requestId: CwpAdRequestId = CwpAdRequestId.NONE,
var adRepo: IAdRepository = IAdRepository.NONE,

var principal: CwpAdPrincipalModel = CwpAdPrincipalModel.NONE,
val permissionsChain: MutableSet<CwpAdUserPermissions> = mutableSetOf(),
var permitted: Boolean = false,

var adRequest: CwpAd = CwpAd(),
var adFilterRequest: CwpAdFilter = CwpAdFilter(),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.crowdproj.ad.common.permissions

import com.crowdproj.ad.common.models.CwpAdUserId

data class CwpAdPrincipalModel(
val id: CwpAdUserId = CwpAdUserId.NONE,
val fname: String = "",
val mname: String = "",
val lname: String = "",
val groups: Set<CwpAdUserGroups> = emptySet()
) {
companion object {
val NONE = CwpAdPrincipalModel()
}
}
Loading

0 comments on commit d1b4577

Please sign in to comment.