diff --git a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20Generator.kt b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20Generator.kt index 065d80d6..2d270866 100644 --- a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20Generator.kt +++ b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20Generator.kt @@ -1,6 +1,8 @@ package com.epages.restdocs.openapi.gradle import com.epages.restdocs.openapi.gradle.schema.JsonSchemaFromFieldDescriptorsGenerator +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.module.kotlin.readValue import io.swagger.models.Info import io.swagger.models.Model @@ -11,6 +13,7 @@ import io.swagger.models.RefModel import io.swagger.models.Response import io.swagger.models.Scheme import io.swagger.models.Swagger +import io.swagger.models.auth.OAuth2Definition import io.swagger.models.parameters.BodyParameter import io.swagger.models.parameters.HeaderParameter import io.swagger.models.parameters.Parameter @@ -21,13 +24,16 @@ import io.swagger.util.Json internal object OpenApi20Generator { + private val objectMapper = ObjectMapper(YAMLFactory()) + fun generate( resources: List, basePath: String? = null, host: String = "localhost", schemes: List = listOf("http"), title: String = "API", - version: String = "1.0.0" + version: String = "1.0.0", + oauth2SecuritySchemeDefinition: Oauth2Configuration? = null ): Swagger { return Swagger().apply { @@ -41,7 +47,7 @@ internal object OpenApi20Generator { paths = generatePaths(resources) extractDefinitions(this) - } + }.apply { addSecurityDefinitions(this, oauth2SecuritySchemeDefinition) } } private fun extractDefinitions(swagger: Swagger): Swagger { @@ -208,6 +214,31 @@ internal object OpenApi20Generator { return if (securityRequirements.type == SecurityType.OAUTH2 && securityRequirements.requiredScopes != null) securityRequirements.requiredScopes else listOf() } + private fun addSecurityDefinitions(openApi: Swagger, oauth2SecuritySchemeDefinition: Oauth2Configuration?) { + oauth2SecuritySchemeDefinition?.flows?.map { f -> + openApi.addSecurityDefinition("oauth2_$f", OAuth2Definition().apply { + flow = f + tokenUrl = oauth2SecuritySchemeDefinition.tokenUrl + val scopeAndDescriptions = oauth2SecuritySchemeDefinition.scopeDescriptionsPropertiesProjectFile + ?.let { objectMapper.readValue>(it) } + ?: emptyMap() + val allScopes = openApi.paths + .flatMap { + it.value.operations + .flatMap { + it?.security + ?.filter { it.containsKey("oauth2") } + ?.flatMap { it.values.flatMap { it } } + ?: listOf() + } + } + allScopes.forEach { + addScope(it, scopeAndDescriptions.getOrDefault(it, "No description") as String) + } + }) + } + } + private fun pathParameterDescriptor2Parameter(parameterDescriptor: ParameterDescriptor): PathParameter { return PathParameter().apply { name = parameterDescriptor.name diff --git a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPlugin.kt b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPlugin.kt index 9cebc079..eba391ac 100644 --- a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPlugin.kt +++ b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPlugin.kt @@ -24,6 +24,8 @@ open class RestdocsOpenApiPlugin : Plugin { apiVersion = openapi.version separatePublicApi = openapi.separatePublicApi + oauth2SecuritySchemeDefinition = openapi.oauth2SecuritySchemeDefinition + outputDirectory = openapi.outputDirectory snippetsDirectory = openapi.snippetsDirectory diff --git a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPluginExtension.kt b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPluginExtension.kt index fe6d2212..511b8ad1 100644 --- a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPluginExtension.kt +++ b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiPluginExtension.kt @@ -1,8 +1,10 @@ package com.epages.restdocs.openapi.gradle +import groovy.lang.Closure import org.gradle.api.Project +import java.io.File -open class RestdocsOpenApiPluginExtension(project: Project) { +open class RestdocsOpenApiPluginExtension(val project: Project) { var host: String = "localhost" var basePath: String? = null var schemes: Array = arrayOf("http") @@ -13,8 +15,30 @@ open class RestdocsOpenApiPluginExtension(project: Project) { var format = "json" var separatePublicApi: Boolean = false + + var oauth2SecuritySchemeDefinition: Oauth2Configuration? = null + var outputDirectory = "build/openapi" var snippetsDirectory = "build/generated-snippets" var outputFileNamePrefix = "openapi" + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + fun setOauth2SecuritySchemeDefinition(closure: Closure<*>) { + oauth2SecuritySchemeDefinition = project.configure(Oauth2Configuration(), closure) as Oauth2Configuration + with(oauth2SecuritySchemeDefinition!!) { + if (scopeDescriptionsPropertiesFile != null) { + scopeDescriptionsPropertiesProjectFile = project.file(scopeDescriptionsPropertiesFile) + } + } + } +} + +class Oauth2Configuration( + var tokenUrl: String = "", + var flows: Array = arrayOf(), + var scopeDescriptionsPropertiesFile: String? = null +) { + internal var + scopeDescriptionsPropertiesProjectFile: File? = null } diff --git a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTask.kt b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTask.kt index 678e86e2..4dc5a4c7 100644 --- a/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTask.kt +++ b/restdocs-openapi-gradle-plugin/src/main/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTask.kt @@ -40,6 +40,9 @@ open class RestdocsOpenApiTask : DefaultTask() { @Input lateinit var outputFileNamePrefix: String + @Input @Optional + var oauth2SecuritySchemeDefinition: Oauth2Configuration? = null + private val outputDirectoryFile get() = project.file(outputDirectory) @@ -71,7 +74,8 @@ open class RestdocsOpenApiTask : DefaultTask() { host = host, schemes = schemes.toList(), title = title, - version = apiVersion + version = apiVersion, + oauth2SecuritySchemeDefinition = oauth2SecuritySchemeDefinition ) ApiSpecificationWriter.write(format, outputDirectoryFile, fileNamePrefix, apiSpecification) diff --git a/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20GeneratorTest.kt b/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20GeneratorTest.kt index e5da4921..d6b82b36 100644 --- a/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20GeneratorTest.kt +++ b/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/OpenApi20GeneratorTest.kt @@ -5,6 +5,7 @@ import io.swagger.models.Model import io.swagger.models.Path import io.swagger.models.Response import io.swagger.models.Swagger +import io.swagger.models.auth.OAuth2Definition import io.swagger.models.parameters.BodyParameter import io.swagger.models.parameters.Parameter import io.swagger.models.parameters.PathParameter @@ -95,6 +96,27 @@ class OpenApi20GeneratorTest { thenOptionsRequestExist(openapi, api) } + @Test + fun `should add security scheme`() { + val api = givenGetProductResourceModel() + + val openapi = OpenApi20Generator.generate( + resources = api, + oauth2SecuritySchemeDefinition = Oauth2Configuration("http://example.com/token", arrayOf("accessCode")) + ) + + println(Json.pretty().writeValueAsString(openapi)) + with(openapi.securityDefinitions) { + then(this.containsKey("name")) + then(this["oauth2_accessCode"]).isEqualTo(OAuth2Definition().apply { + tokenUrl = "http://example.com/token" + flow = "accessCode" + scope("prod:r", "No description") + }) + } + thenValidateOpenApi(openapi) + } + private fun thenOptionsRequestExist(openapi: Swagger, api: List) { then(openapi.getPath(api.get(0).request.path).options).isNotNull() } diff --git a/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTaskTest.kt b/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTaskTest.kt index 43487b7c..944a5406 100644 --- a/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTaskTest.kt +++ b/restdocs-openapi-gradle-plugin/src/test/kotlin/com/epages/restdocs/openapi/gradle/RestdocsOpenApiTaskTest.kt @@ -2,6 +2,7 @@ package com.epages.restdocs.openapi.gradle import com.epages.restdocs.openapi.gradle.junit.TemporaryFolder import com.epages.restdocs.openapi.gradle.junit.TemporaryFolderExtension +import com.jayway.jsonpath.JsonPath import org.assertj.core.api.BDDAssertions.then import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner @@ -62,7 +63,7 @@ class RestdocsOpenApiTaskTest(private val testProjectDir: TemporaryFolder) { whenPluginExecuted() - thenTaskSuccessful() + thenOpenApiTaskSuccessful() thenOutputFileFound() thenOutputFileForPublicResourceSpecificationNotFound() } @@ -75,7 +76,7 @@ class RestdocsOpenApiTaskTest(private val testProjectDir: TemporaryFolder) { whenPluginExecuted() - thenTaskSuccessful() + thenOpenApiTaskSuccessful() thenOutputFileFound() } @@ -88,13 +89,39 @@ class RestdocsOpenApiTaskTest(private val testProjectDir: TemporaryFolder) { whenPluginExecuted() - thenTaskSuccessful() + thenOpenApiTaskSuccessful() thenOutputFileFound() thenOutputFileForPublicResourceSpecificationFound() } - private fun thenTaskSuccessful() { - then(result.task(":openapi")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + @Test + fun `should consider security definitions`() { + givenBuildFileWithOpenApiClosureAndSecurityDefintions() + givenResourceSnippet() + givenScopeTextFile() + + whenPluginExecuted() + + thenOpenApiTaskSuccessful() + thenOutputFileFound() + thenSecurityDefinitionsFoundInOutputFile() + } + + private fun thenSecurityDefinitionsFoundInOutputFile() { + with(JsonPath.parse(outputFolder.resolve("$outputFileNamePrefix.$format").readText())) { + then(read("securityDefinitions.oauth2_accessCode.scopes.prod:r")).isEqualTo("Some text") + then(read("securityDefinitions.oauth2_accessCode.type")).isEqualTo("oauth2") + then(read("securityDefinitions.oauth2_accessCode.tokenUrl")).isNotEmpty() + then(read("securityDefinitions.oauth2_accessCode.flow")).isNotEmpty() + } + } + + private fun givenScopeTextFile() { + File(testProjectDir.root, "scopeDescriptions.yaml").writeText( + """ + "prod:r": "Some text" + """.trimIndent() + ) } private fun thenOpenApiTaskSuccessful() { @@ -176,7 +203,10 @@ class RestdocsOpenApiTaskTest(private val testProjectDir: TemporaryFolder) { "requestParameters" : [ ], "requestFields" : [ ], "example" : null, - "securityRequirements" : null + "securityRequirements" : { + "type": "OAUTH2", + "requiredScopes": ["prod:r"] + } }, "response" : { "status" : 200, @@ -207,7 +237,27 @@ class RestdocsOpenApiTaskTest(private val testProjectDir: TemporaryFolder) { outputFileNamePrefix = '$outputFileNamePrefix' } """.trimIndent()) -} + } + + private fun givenBuildFileWithOpenApiClosureAndSecurityDefintions() { + buildFile.writeText(baseBuildFile() + """ + openapi { + host = '$host' + basePath = '$basePath' + schemes = ${schemes.joinToString(",", "['", "']")} + title = '$title' + version = '$version' + format = '$format' + separatePublicApi = $separatePublicApi + outputFileNamePrefix = '$outputFileNamePrefix' + oauth2SecuritySchemeDefinition = { + flows = ['accessCode'] + tokenUrl = 'http://example.com/token' + scopeDescriptionsPropertiesFile = "scopeDescriptions.yaml" + } + } + """.trimIndent()) + } private fun baseBuildFile() = """ plugins {