Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -21,13 +24,16 @@ import io.swagger.util.Json

internal object OpenApi20Generator {

private val objectMapper = ObjectMapper(YAMLFactory())

fun generate(
resources: List<ResourceModel>,
basePath: String? = null,
host: String = "localhost",
schemes: List<String> = listOf("http"),
title: String = "API",
version: String = "1.0.0"
version: String = "1.0.0",
oauth2SecuritySchemeDefinition: Oauth2Configuration? = null
): Swagger {
return Swagger().apply {

Expand All @@ -41,7 +47,7 @@ internal object OpenApi20Generator {
paths = generatePaths(resources)

extractDefinitions(this)
}
}.apply { addSecurityDefinitions(this, oauth2SecuritySchemeDefinition) }
}

private fun extractDefinitions(swagger: Swagger): Swagger {
Expand Down Expand Up @@ -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<Map<String, Any>>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ open class RestdocsOpenApiPlugin : Plugin<Project> {
apiVersion = openapi.version
separatePublicApi = openapi.separatePublicApi

oauth2SecuritySchemeDefinition = openapi.oauth2SecuritySchemeDefinition

outputDirectory = openapi.outputDirectory
snippetsDirectory = openapi.snippetsDirectory

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> = arrayOf("http")
Expand All @@ -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<String> = arrayOf(),
var scopeDescriptionsPropertiesFile: String? = null
) {
internal var
scopeDescriptionsPropertiesProjectFile: File? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ResourceModel>) {
then(openapi.getPath(api.get(0).request.path).options).isNotNull()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,7 +63,7 @@ class RestdocsOpenApiTaskTest(private val testProjectDir: TemporaryFolder) {

whenPluginExecuted()

thenTaskSuccessful()
thenOpenApiTaskSuccessful()
thenOutputFileFound()
thenOutputFileForPublicResourceSpecificationNotFound()
}
Expand All @@ -75,7 +76,7 @@ class RestdocsOpenApiTaskTest(private val testProjectDir: TemporaryFolder) {

whenPluginExecuted()

thenTaskSuccessful()
thenOpenApiTaskSuccessful()
thenOutputFileFound()
}

Expand All @@ -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<String>("securityDefinitions.oauth2_accessCode.scopes.prod:r")).isEqualTo("Some text")
then(read<String>("securityDefinitions.oauth2_accessCode.type")).isEqualTo("oauth2")
then(read<String>("securityDefinitions.oauth2_accessCode.tokenUrl")).isNotEmpty()
then(read<String>("securityDefinitions.oauth2_accessCode.flow")).isNotEmpty()
}
}

private fun givenScopeTextFile() {
File(testProjectDir.root, "scopeDescriptions.yaml").writeText(
"""
"prod:r": "Some text"
""".trimIndent()
)
}

private fun thenOpenApiTaskSuccessful() {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down