init security scheme (#29)
This commit is contained in:
@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.6.0] - April 21st, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added basic and jwt security scheme support with the new module kompendium-auth
|
||||||
|
|
||||||
## [0.5.2] - April 19th, 2021
|
## [0.5.2] - April 19th, 2021
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
50
README.md
50
README.md
@ -41,7 +41,6 @@ dependencies {
|
|||||||
Kompendium is still under active development ⚠️ There are a number of yet-to-be-implemented features, including
|
Kompendium is still under active development ⚠️ There are a number of yet-to-be-implemented features, including
|
||||||
|
|
||||||
- Multiple Responses 📜
|
- Multiple Responses 📜
|
||||||
- Security Schemas 🔏
|
|
||||||
- Sealed Class / Polymorphic Support 😬
|
- Sealed Class / Polymorphic Support 😬
|
||||||
- Validation / Enforcement (❓👀❓)
|
- Validation / Enforcement (❓👀❓)
|
||||||
|
|
||||||
@ -135,6 +134,55 @@ When run in the playground, this would output the following at `/openapi.json`
|
|||||||
|
|
||||||
https://gist.github.com/rgbrizzlehizzle/b9544922f2e99a2815177f8bdbf80668
|
https://gist.github.com/rgbrizzlehizzle/b9544922f2e99a2815177f8bdbf80668
|
||||||
|
|
||||||
|
### Kompendium Auth and security schemes
|
||||||
|
|
||||||
|
There is a seperate library to handle security schemes: `kompendium-auth`.
|
||||||
|
This needs to be added to your project as dependency.
|
||||||
|
|
||||||
|
At the moment, the basic and jwt authentication is only supported.
|
||||||
|
|
||||||
|
A minimal example would be:
|
||||||
|
```kotlin
|
||||||
|
install(Authentication) {
|
||||||
|
notarizedBasic("basic") {
|
||||||
|
realm = "Ktor realm 1"
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
notarizedJwt("jwt") {
|
||||||
|
realm = "Ktor realm 2"
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routing {
|
||||||
|
authenticate("basic") {
|
||||||
|
route("/basic_auth") {
|
||||||
|
notarizedGet<TestParams, TestResponse>(
|
||||||
|
MethodInfo(
|
||||||
|
// securitySchemes needs to be set
|
||||||
|
"Another get test", "testing more", testGetResponse, securitySchemes = setOf("basic")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
call.respondText { "basic auth" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authenticate("jwt") {
|
||||||
|
route("/jwt") {
|
||||||
|
notarizedGet<TestParams, TestResponse>(
|
||||||
|
MethodInfo(
|
||||||
|
// securitySchemes needs to be set
|
||||||
|
"Another get test", "testing more", testGetResponse, securitySchemes = setOf("jwt")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
call.respondText { "jwt" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
### Kompendium as a singleton
|
### Kompendium as a singleton
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=0.5.2
|
project.version=0.6.0
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
|
@ -10,6 +10,9 @@ ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref =
|
|||||||
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
|
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
|
||||||
ktor-jackson = { group = "io.ktor", name = "ktor-jackson", version.ref = "ktor" }
|
ktor-jackson = { group = "io.ktor", name = "ktor-jackson", version.ref = "ktor" }
|
||||||
ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref = "ktor" }
|
ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref = "ktor" }
|
||||||
|
ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" }
|
||||||
|
ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
||||||
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
||||||
@ -17,4 +20,5 @@ logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref =
|
|||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ]
|
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ]
|
||||||
|
ktorAuth = [ "ktor-auth-lib", "ktor-auth-jwt" ]
|
||||||
logging = [ "slf4j", "logback-classic", "logback-core" ]
|
logging = [ "slf4j", "logback-classic", "logback-core" ]
|
||||||
|
40
kompendium-auth/build.gradle.kts
Normal file
40
kompendium-auth/build.gradle.kts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
plugins {
|
||||||
|
`java-library`
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
implementation(libs.bundles.ktor)
|
||||||
|
implementation(libs.bundles.ktorAuth)
|
||||||
|
implementation(projects.kompendiumCore)
|
||||||
|
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||||
|
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
|
||||||
|
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = "GithubPackages"
|
||||||
|
url = uri("https://maven.pkg.github.com/lg-backbone/kompendium")
|
||||||
|
credentials {
|
||||||
|
username = System.getenv("GITHUB_ACTOR")
|
||||||
|
password = System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("kompendium") {
|
||||||
|
from(components["kotlin"])
|
||||||
|
artifact(tasks.sourcesJar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package org.leafygreens.kompendium.auth
|
||||||
|
|
||||||
|
import io.ktor.auth.AuthenticationRouteSelector
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import org.leafygreens.kompendium.path.CorePathCalculator
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
class AuthPathCalculator : CorePathCalculator() {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
override fun handleCustomSelectors(route: Route, tail: String): String = when (route.selector) {
|
||||||
|
is AuthenticationRouteSelector -> {
|
||||||
|
logger.debug("Found authentication route selector ${route.selector}")
|
||||||
|
super.calculate(route.parent, tail)
|
||||||
|
}
|
||||||
|
else -> super.handleCustomSelectors(route, tail)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.leafygreens.kompendium.auth
|
||||||
|
|
||||||
|
import io.ktor.auth.Authentication
|
||||||
|
import io.ktor.auth.basic
|
||||||
|
import io.ktor.auth.BasicAuthenticationProvider
|
||||||
|
import io.ktor.auth.jwt.jwt
|
||||||
|
import io.ktor.auth.jwt.JWTAuthenticationProvider
|
||||||
|
import org.leafygreens.kompendium.Kompendium
|
||||||
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
||||||
|
|
||||||
|
object KompendiumAuth {
|
||||||
|
|
||||||
|
init {
|
||||||
|
Kompendium.pathCalculator = AuthPathCalculator()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Authentication.Configuration.notarizedBasic(
|
||||||
|
name: String? = null,
|
||||||
|
configure: BasicAuthenticationProvider.Configuration.() -> Unit
|
||||||
|
) {
|
||||||
|
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
|
||||||
|
type = "http",
|
||||||
|
scheme = "basic"
|
||||||
|
)
|
||||||
|
basic(name, configure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Authentication.Configuration.notarizedJwt(
|
||||||
|
name: String? = null,
|
||||||
|
header: String? = null,
|
||||||
|
scheme: String? = null,
|
||||||
|
configure: JWTAuthenticationProvider.Configuration.() -> Unit
|
||||||
|
) {
|
||||||
|
if (header == null || header == "Authorization") {
|
||||||
|
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
|
||||||
|
type = "http",
|
||||||
|
scheme = scheme ?: "bearer"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
|
||||||
|
type = "apiKey",
|
||||||
|
name = header,
|
||||||
|
`in` = "header"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
jwt(name, configure)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO support other authentication providers (e.g., oAuth)?
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
package org.leafygreens.kompendium.auth
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
|
import io.ktor.application.*
|
||||||
|
import io.ktor.auth.*
|
||||||
|
import io.ktor.features.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.jackson.*
|
||||||
|
import io.ktor.response.*
|
||||||
|
import io.ktor.routing.*
|
||||||
|
import io.ktor.server.testing.*
|
||||||
|
import org.junit.Test
|
||||||
|
import org.leafygreens.kompendium.Kompendium
|
||||||
|
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
||||||
|
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
|
||||||
|
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedJwt
|
||||||
|
import org.leafygreens.kompendium.auth.util.TestData
|
||||||
|
import org.leafygreens.kompendium.auth.util.TestParams
|
||||||
|
import org.leafygreens.kompendium.auth.util.TestResponse
|
||||||
|
import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||||
|
import org.leafygreens.kompendium.models.meta.ResponseInfo
|
||||||
|
import org.leafygreens.kompendium.models.oas.OpenApiSpec
|
||||||
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
||||||
|
import org.leafygreens.kompendium.routes.openApi
|
||||||
|
import org.leafygreens.kompendium.routes.redoc
|
||||||
|
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||||
|
import kotlin.test.AfterTest
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
internal class KompendiumAuthTest {
|
||||||
|
|
||||||
|
@AfterTest
|
||||||
|
fun `reset kompendium`() {
|
||||||
|
Kompendium.openApiSpec = OpenApiSpec(
|
||||||
|
info = OpenApiSpecInfo(),
|
||||||
|
servers = mutableListOf(),
|
||||||
|
paths = mutableMapOf()
|
||||||
|
)
|
||||||
|
Kompendium.cache = emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Notarized Get with basic authentication records all expected information`() {
|
||||||
|
withTestApplication({
|
||||||
|
configModule()
|
||||||
|
configBasicAuth()
|
||||||
|
docs()
|
||||||
|
notarizedAuthenticatedGetModule(TestData.AuthConfigName.Basic)
|
||||||
|
}) {
|
||||||
|
// do
|
||||||
|
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||||
|
|
||||||
|
// expect
|
||||||
|
val expected = TestData.getFileSnapshot("notarized_basic_authenticated_get.json").trim()
|
||||||
|
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Notarized Get with jwt authentication records all expected information`() {
|
||||||
|
withTestApplication({
|
||||||
|
configModule()
|
||||||
|
configJwtAuth()
|
||||||
|
docs()
|
||||||
|
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
|
||||||
|
}) {
|
||||||
|
// do
|
||||||
|
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||||
|
|
||||||
|
// expect
|
||||||
|
val expected = TestData.getFileSnapshot("notarized_jwt_authenticated_get.json").trim()
|
||||||
|
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Notarized Get with jwt authentication and custom scheme records all expected information`() {
|
||||||
|
withTestApplication({
|
||||||
|
configModule()
|
||||||
|
configJwtAuth(scheme = "oauth")
|
||||||
|
docs()
|
||||||
|
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
|
||||||
|
}) {
|
||||||
|
// do
|
||||||
|
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||||
|
|
||||||
|
// expect
|
||||||
|
val expected = TestData.getFileSnapshot("notarized_jwt_custom_scheme_authenticated_get.json").trim()
|
||||||
|
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Notarized Get with jwt authentication and custom header records all expected information`() {
|
||||||
|
withTestApplication({
|
||||||
|
configModule()
|
||||||
|
configJwtAuth(header = "x-api-key")
|
||||||
|
docs()
|
||||||
|
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
|
||||||
|
}) {
|
||||||
|
// do
|
||||||
|
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||||
|
|
||||||
|
// expect
|
||||||
|
val expected = TestData.getFileSnapshot("notarized_jwt_custom_header_authenticated_get.json").trim()
|
||||||
|
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Notarized Get with multiple jwt schemes records all expected information`() {
|
||||||
|
withTestApplication({
|
||||||
|
configModule()
|
||||||
|
install(Authentication) {
|
||||||
|
notarizedJwt("jwt1", header = "x-api-key-1") {
|
||||||
|
realm = "Ktor server"
|
||||||
|
}
|
||||||
|
notarizedJwt("jwt2", header = "x-api-key-2") {
|
||||||
|
realm = "Ktor server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
docs()
|
||||||
|
notarizedAuthenticatedGetModule("jwt1", "jwt2")
|
||||||
|
}) {
|
||||||
|
// do
|
||||||
|
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||||
|
|
||||||
|
// expect
|
||||||
|
val expected = TestData.getFileSnapshot("notarized_multiple_jwt_authenticated_get.json").trim()
|
||||||
|
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Application.configModule() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
jackson(ContentType.Application.Json) {
|
||||||
|
enable(SerializationFeature.INDENT_OUTPUT)
|
||||||
|
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Application.configBasicAuth() {
|
||||||
|
install(Authentication) {
|
||||||
|
notarizedBasic(TestData.AuthConfigName.Basic) {
|
||||||
|
realm = "Ktor Server"
|
||||||
|
validate { credentials ->
|
||||||
|
if (credentials.name == credentials.password) {
|
||||||
|
UserIdPrincipal(credentials.name)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Application.configJwtAuth(
|
||||||
|
header: String? = null,
|
||||||
|
scheme: String? = null
|
||||||
|
) {
|
||||||
|
install(Authentication) {
|
||||||
|
notarizedJwt(TestData.AuthConfigName.JWT, header, scheme) {
|
||||||
|
realm = "Ktor server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Application.notarizedAuthenticatedGetModule(vararg authenticationConfigName: String) {
|
||||||
|
routing {
|
||||||
|
authenticate(*authenticationConfigName) {
|
||||||
|
route(TestData.getRoutePath) {
|
||||||
|
notarizedGet<TestParams, TestResponse>(testGetInfo(*authenticationConfigName)) {
|
||||||
|
call.respondText { "hey dude ‼️ congratz on the get request" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val oas = Kompendium.openApiSpec.copy()
|
||||||
|
|
||||||
|
private fun Application.docs() {
|
||||||
|
routing {
|
||||||
|
openApi(oas)
|
||||||
|
redoc(oas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val testGetResponse = ResponseInfo(KompendiumHttpCodes.OK, "A Successful Endeavor")
|
||||||
|
fun testGetInfo(vararg security: String) =
|
||||||
|
MethodInfo("Another get test", "testing more", testGetResponse, securitySchemes = security.toSet())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package org.leafygreens.kompendium.auth.util
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object TestData {
|
||||||
|
object AuthConfigName {
|
||||||
|
val Basic = "basic"
|
||||||
|
val JWT = "jwt"
|
||||||
|
}
|
||||||
|
|
||||||
|
val getRoutePath = "/test"
|
||||||
|
|
||||||
|
fun getFileSnapshot(fileName: String): String {
|
||||||
|
val snapshotPath = "src/test/resources"
|
||||||
|
val file = File("$snapshotPath/$fileName")
|
||||||
|
return file.readText()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package org.leafygreens.kompendium.auth.util
|
||||||
|
|
||||||
|
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||||
|
import org.leafygreens.kompendium.annotations.PathParam
|
||||||
|
import org.leafygreens.kompendium.annotations.QueryParam
|
||||||
|
|
||||||
|
data class TestParams(
|
||||||
|
@PathParam val a: String,
|
||||||
|
@QueryParam val aa: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TestRequest(
|
||||||
|
@KompendiumField(name = "field_name")
|
||||||
|
val b: Double,
|
||||||
|
val aaa: List<Long>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TestResponse(val c: String)
|
||||||
|
|
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"openapi" : "3.0.3",
|
||||||
|
"info" : { },
|
||||||
|
"servers" : [ ],
|
||||||
|
"paths" : {
|
||||||
|
"/test" : {
|
||||||
|
"get" : {
|
||||||
|
"tags" : [ ],
|
||||||
|
"summary" : "Another get test",
|
||||||
|
"description" : "testing more",
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "a",
|
||||||
|
"in" : "path",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
}, {
|
||||||
|
"name" : "aa",
|
||||||
|
"in" : "query",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/Int"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
} ],
|
||||||
|
"responses" : {
|
||||||
|
"200" : {
|
||||||
|
"description" : "A Successful Endeavor",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/TestResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated" : false,
|
||||||
|
"security" : [ {
|
||||||
|
"basic" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components" : {
|
||||||
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"TestResponse" : {
|
||||||
|
"properties" : {
|
||||||
|
"c" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes" : {
|
||||||
|
"basic" : {
|
||||||
|
"type" : "http",
|
||||||
|
"scheme" : "basic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security" : [ ],
|
||||||
|
"tags" : [ ]
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"openapi" : "3.0.3",
|
||||||
|
"info" : { },
|
||||||
|
"servers" : [ ],
|
||||||
|
"paths" : {
|
||||||
|
"/test" : {
|
||||||
|
"get" : {
|
||||||
|
"tags" : [ ],
|
||||||
|
"summary" : "Another get test",
|
||||||
|
"description" : "testing more",
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "a",
|
||||||
|
"in" : "path",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
}, {
|
||||||
|
"name" : "aa",
|
||||||
|
"in" : "query",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/Int"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
} ],
|
||||||
|
"responses" : {
|
||||||
|
"200" : {
|
||||||
|
"description" : "A Successful Endeavor",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/TestResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated" : false,
|
||||||
|
"security" : [ {
|
||||||
|
"jwt" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components" : {
|
||||||
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"TestResponse" : {
|
||||||
|
"properties" : {
|
||||||
|
"c" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes" : {
|
||||||
|
"jwt" : {
|
||||||
|
"type" : "http",
|
||||||
|
"scheme" : "bearer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security" : [ ],
|
||||||
|
"tags" : [ ]
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"openapi" : "3.0.3",
|
||||||
|
"info" : { },
|
||||||
|
"servers" : [ ],
|
||||||
|
"paths" : {
|
||||||
|
"/test" : {
|
||||||
|
"get" : {
|
||||||
|
"tags" : [ ],
|
||||||
|
"summary" : "Another get test",
|
||||||
|
"description" : "testing more",
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "a",
|
||||||
|
"in" : "path",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
}, {
|
||||||
|
"name" : "aa",
|
||||||
|
"in" : "query",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/Int"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
} ],
|
||||||
|
"responses" : {
|
||||||
|
"200" : {
|
||||||
|
"description" : "A Successful Endeavor",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/TestResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated" : false,
|
||||||
|
"security" : [ {
|
||||||
|
"jwt" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components" : {
|
||||||
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"TestResponse" : {
|
||||||
|
"properties" : {
|
||||||
|
"c" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes" : {
|
||||||
|
"jwt" : {
|
||||||
|
"type" : "apiKey",
|
||||||
|
"name" : "x-api-key",
|
||||||
|
"in" : "header"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security" : [ ],
|
||||||
|
"tags" : [ ]
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"openapi" : "3.0.3",
|
||||||
|
"info" : { },
|
||||||
|
"servers" : [ ],
|
||||||
|
"paths" : {
|
||||||
|
"/test" : {
|
||||||
|
"get" : {
|
||||||
|
"tags" : [ ],
|
||||||
|
"summary" : "Another get test",
|
||||||
|
"description" : "testing more",
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "a",
|
||||||
|
"in" : "path",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
}, {
|
||||||
|
"name" : "aa",
|
||||||
|
"in" : "query",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/Int"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
} ],
|
||||||
|
"responses" : {
|
||||||
|
"200" : {
|
||||||
|
"description" : "A Successful Endeavor",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/TestResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated" : false,
|
||||||
|
"security" : [ {
|
||||||
|
"jwt" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components" : {
|
||||||
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"TestResponse" : {
|
||||||
|
"properties" : {
|
||||||
|
"c" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes" : {
|
||||||
|
"jwt" : {
|
||||||
|
"type" : "http",
|
||||||
|
"scheme" : "oauth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security" : [ ],
|
||||||
|
"tags" : [ ]
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"openapi" : "3.0.3",
|
||||||
|
"info" : { },
|
||||||
|
"servers" : [ ],
|
||||||
|
"paths" : {
|
||||||
|
"/test" : {
|
||||||
|
"get" : {
|
||||||
|
"tags" : [ ],
|
||||||
|
"summary" : "Another get test",
|
||||||
|
"description" : "testing more",
|
||||||
|
"parameters" : [ {
|
||||||
|
"name" : "a",
|
||||||
|
"in" : "path",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
}, {
|
||||||
|
"name" : "aa",
|
||||||
|
"in" : "query",
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/Int"
|
||||||
|
},
|
||||||
|
"required" : true,
|
||||||
|
"deprecated" : false
|
||||||
|
} ],
|
||||||
|
"responses" : {
|
||||||
|
"200" : {
|
||||||
|
"description" : "A Successful Endeavor",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/TestResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated" : false,
|
||||||
|
"security" : [ {
|
||||||
|
"jwt1" : [ ],
|
||||||
|
"jwt2" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components" : {
|
||||||
|
"schemas" : {
|
||||||
|
"String" : {
|
||||||
|
"type" : "string"
|
||||||
|
},
|
||||||
|
"TestResponse" : {
|
||||||
|
"properties" : {
|
||||||
|
"c" : {
|
||||||
|
"$ref" : "#/components/schemas/String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type" : "object"
|
||||||
|
},
|
||||||
|
"Int" : {
|
||||||
|
"format" : "int32",
|
||||||
|
"type" : "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes" : {
|
||||||
|
"jwt1" : {
|
||||||
|
"type" : "apiKey",
|
||||||
|
"name" : "x-api-key-1",
|
||||||
|
"in" : "header"
|
||||||
|
},
|
||||||
|
"jwt2" : {
|
||||||
|
"type" : "apiKey",
|
||||||
|
"name" : "x-api-key-2",
|
||||||
|
"in" : "header"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security" : [ ],
|
||||||
|
"tags" : [ ]
|
||||||
|
}
|
@ -101,7 +101,11 @@ object Kompendium {
|
|||||||
deprecated = this.deprecated,
|
deprecated = this.deprecated,
|
||||||
parameters = paramType.toParameterSpec(),
|
parameters = paramType.toParameterSpec(),
|
||||||
responses = responseType.toResponseSpec(responseInfo)?.let { mapOf(it) },
|
responses = responseType.toResponseSpec(responseInfo)?.let { mapOf(it) },
|
||||||
requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null
|
requestBody = if (method != HttpMethod.Get) requestType.toRequestSpec(requestInfo) else null,
|
||||||
|
security = if (this.securitySchemes.isNotEmpty()) listOf(
|
||||||
|
// TODO support scopes
|
||||||
|
this.securitySchemes.associateWith { listOf() }
|
||||||
|
) else null
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
@ -7,5 +7,6 @@ data class MethodInfo(
|
|||||||
val responseInfo: ResponseInfo? = null,
|
val responseInfo: ResponseInfo? = null,
|
||||||
val requestInfo: RequestInfo? = null,
|
val requestInfo: RequestInfo? = null,
|
||||||
val tags: Set<String> = emptySet(),
|
val tags: Set<String> = emptySet(),
|
||||||
val deprecated: Boolean = false
|
val deprecated: Boolean = false,
|
||||||
|
val securitySchemes: Set<String> = emptySet()
|
||||||
)
|
)
|
||||||
|
@ -3,5 +3,5 @@ package org.leafygreens.kompendium.models.oas
|
|||||||
// TODO I *think* the only thing I need here is the security https://swagger.io/specification/#components-object
|
// TODO I *think* the only thing I need here is the security https://swagger.io/specification/#components-object
|
||||||
data class OpenApiSpecComponents(
|
data class OpenApiSpecComponents(
|
||||||
val schemas: MutableMap<String, OpenApiSpecComponentSchema> = mutableMapOf(),
|
val schemas: MutableMap<String, OpenApiSpecComponentSchema> = mutableMapOf(),
|
||||||
val securitySchemes: MutableMap<String, OpenApiSpecSchema> = mutableMapOf()
|
val securitySchemes: MutableMap<String, OpenApiSpecSchemaSecurity> = mutableMapOf()
|
||||||
)
|
)
|
||||||
|
@ -7,8 +7,10 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
|
||||||
implementation(projects.kompendiumCore)
|
implementation(projects.kompendiumCore)
|
||||||
|
implementation(projects.kompendiumAuth)
|
||||||
|
|
||||||
implementation(libs.bundles.ktor)
|
implementation(libs.bundles.ktor)
|
||||||
|
implementation(libs.bundles.ktorAuth)
|
||||||
implementation(libs.bundles.logging)
|
implementation(libs.bundles.logging)
|
||||||
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
@ -5,6 +5,9 @@ import com.fasterxml.jackson.databind.SerializationFeature
|
|||||||
import io.ktor.application.Application
|
import io.ktor.application.Application
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.application.install
|
import io.ktor.application.install
|
||||||
|
import io.ktor.auth.Authentication
|
||||||
|
import io.ktor.auth.authenticate
|
||||||
|
import io.ktor.auth.UserIdPrincipal
|
||||||
import io.ktor.features.ContentNegotiation
|
import io.ktor.features.ContentNegotiation
|
||||||
import io.ktor.jackson.jackson
|
import io.ktor.jackson.jackson
|
||||||
import io.ktor.response.respondText
|
import io.ktor.response.respondText
|
||||||
@ -18,6 +21,7 @@ import org.leafygreens.kompendium.Kompendium.notarizedDelete
|
|||||||
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
import org.leafygreens.kompendium.Kompendium.notarizedGet
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
import org.leafygreens.kompendium.Kompendium.notarizedPost
|
||||||
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
import org.leafygreens.kompendium.Kompendium.notarizedPut
|
||||||
|
import org.leafygreens.kompendium.auth.KompendiumAuth.notarizedBasic
|
||||||
import org.leafygreens.kompendium.annotations.KompendiumField
|
import org.leafygreens.kompendium.annotations.KompendiumField
|
||||||
import org.leafygreens.kompendium.annotations.PathParam
|
import org.leafygreens.kompendium.annotations.PathParam
|
||||||
import org.leafygreens.kompendium.annotations.QueryParam
|
import org.leafygreens.kompendium.annotations.QueryParam
|
||||||
@ -28,6 +32,7 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
|||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
||||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
|
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
|
||||||
|
import org.leafygreens.kompendium.playground.KompendiumTOC.testAuthenticatedSingleGetInfo
|
||||||
import org.leafygreens.kompendium.playground.KompendiumTOC.testIdGetInfo
|
import org.leafygreens.kompendium.playground.KompendiumTOC.testIdGetInfo
|
||||||
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo
|
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleDeleteInfo
|
||||||
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfo
|
import org.leafygreens.kompendium.playground.KompendiumTOC.testSingleGetInfo
|
||||||
@ -73,12 +78,29 @@ fun main() {
|
|||||||
).start(wait = true)
|
).start(wait = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var featuresInstalled = false
|
||||||
fun Application.mainModule() {
|
fun Application.mainModule() {
|
||||||
install(ContentNegotiation) {
|
// only install once in case of auto reload
|
||||||
jackson {
|
if (!featuresInstalled) {
|
||||||
enable(SerializationFeature.INDENT_OUTPUT)
|
install(ContentNegotiation) {
|
||||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
jackson {
|
||||||
|
enable(SerializationFeature.INDENT_OUTPUT)
|
||||||
|
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
install(Authentication) {
|
||||||
|
notarizedBasic("basic") {
|
||||||
|
realm = "Ktor Server"
|
||||||
|
validate { credentials ->
|
||||||
|
if (credentials.name == credentials.password) {
|
||||||
|
UserIdPrincipal(credentials.name)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
featuresInstalled = true
|
||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
openApi(oas)
|
openApi(oas)
|
||||||
@ -103,6 +125,13 @@ fun Application.mainModule() {
|
|||||||
call.respondText { "heya" }
|
call.respondText { "heya" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
authenticate("basic") {
|
||||||
|
route("/authenticated/single") {
|
||||||
|
notarizedGet<Unit, Unit>(testAuthenticatedSingleGetInfo) {
|
||||||
|
call.respondText("get authentiticated single")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,4 +209,14 @@ object KompendiumTOC {
|
|||||||
mediaTypes = emptyList()
|
mediaTypes = emptyList()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val testAuthenticatedSingleGetInfo = MethodInfo(
|
||||||
|
summary = "Another get test",
|
||||||
|
description = "testing more",
|
||||||
|
tags = setOf("anotherTest", "sample"),
|
||||||
|
responseInfo = ResponseInfo(
|
||||||
|
status = KompendiumHttpCodes.OK,
|
||||||
|
description = "Returns a different sample"
|
||||||
|
),
|
||||||
|
securitySchemes = setOf("basic")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
rootProject.name = "kompendium"
|
rootProject.name = "kompendium"
|
||||||
include("kompendium-core")
|
include("kompendium-core")
|
||||||
|
include("kompendium-auth")
|
||||||
include("kompendium-playground")
|
include("kompendium-playground")
|
||||||
|
|
||||||
// Feature Previews
|
// Feature Previews
|
||||||
|
Reference in New Issue
Block a user