Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
107 changes: 100 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,16 @@ Options:
| info.title | custom.documentation.title OR service |
| info.description | custom.documentation.description OR blank string |
| info.version | custom.documentation.version OR random v4 uuid if not provided |
| info.termsOfService | custom.documentation.termsOfService |
| info.contact | custom.documentation.contact |
| info.contact.name | custom.documentation.contact.name OR blank string |
| info.contact.url | custom.documentation.contact.url if provided |
| info.license | custom.documentation.license |
| info.license.name | custom.documentation.license.name OR blank string |
| info.license.url | custom.documentation.license.url if provided |
| info.termsOfService | custom.documentation.termsOfService |
| info.contact | custom.documentation.contact |
| info.contact.name | custom.documentation.contact.name OR blank string |
| info.contact.url | custom.documentation.contact.url if provided |
| info.license | custom.documentation.license |
| info.license.name | custom.documentation.license.name OR blank string |
| info.license.url | custom.documentation.license.url if provided |
| externalDocs.description | custom.documentation.externalDocumentation.description |
| externalDocs.url | custom.documentation.externalDocumentation.url |
| security | custom.documentation.security |
| servers[].description | custom.documentation.servers.description |
| servers[].url | custom.documentation.servers.url |
| servers[].variables | custom.documentation.servers.variables |
Expand All @@ -89,6 +90,7 @@ Options:
| path[path].[operation].externalDocs.url | functions.functions.[http OR httpApi].documentation.externalDocumentation.url |
| path[path].[operation].servers[].description | functions.functions.[http OR httpApi].documentation.servers.description |
| path[path].[operation].servers[].url | functions.functions.[http OR httpApi].documentation.servers.url |
| path[path].[operation].security | functions.functions.[http OR httpApi].documentation.security |
| path[path].[operation].deprecated | functions.functions.[http OR httpApi].documentation.deprecated |
| path[path].[operation].parameters | functions.functions.[http OR httpApi].documentation.[path/query/cookie/header]Params |
| path[path].[operation].parameters.name | functions.functions.[http OR httpApi].documentation.[path/query/cookie/header]Params.name |
Expand Down Expand Up @@ -219,6 +221,40 @@ functions:

For more info on `serverless.yml` syntax, see their docs.

#### securitySchemes

You can provide optional Security Schemes:

```yml
custom:
documentation:
securitySchemes:
my_api_key:
type: apiKey
name: api_key
in: header
```

It accepts all available Security Schemes and follows the specification: https://spec.openapis.org/oas/v3.0.3#security-scheme-object

#### Security on each operation

To apply an overall security scheme to all of your operations without having to add the documentation to each one, you can write it like:

```yml
custom:
documentation:
securitySchemes:
my_api_key:
type: apiKey
name: api_key
in: header
security:
- my_api_key: []
```

This will apply the requirement of each operation requiring your `my_api_key` security scheme, [you can override this](#security).

#### Models

There are two ways to write the Models. Models contain additional information that you can use to define schemas for endpoints. You must define the *content type* for each schema that you provide in the models.
Expand Down Expand Up @@ -340,6 +376,7 @@ The `documentation` section of the event configuration can contain the following
* `pathParams`: a list of path parameters (see [pathParams](#pathparams) below)
* `cookieParams`: a list of cookie parameters (see [cookieParams](#cookieparams) below)
* `headerParams`: a list of headers (see [headerParams](#headerparams---request-headers) below)
* `security`: The security requirement to apply (see [security](#security) below)
* `methodResponses`: an array of response models and applicable status codes
* `statusCode`: applicable http status code (ie. 200/404/500 etc.)
* `responseBody`: contains description of the response
Expand Down Expand Up @@ -480,6 +517,62 @@ headerParams:
type: "string"
```

#### `security`

The `security` property allows you to specify the [Security Scheme](#securityschemes) to apply to the HTTP Request. If you have applied an `security` ([see Security on each operation](#security-on-each-operation)) then you can either leave this field off, or to override it with a different scheme you can write it like:

```yml
custom:
documentation:
securitySchemes:
my_api_key:
type: apiKey
name: api_key
in: header
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: https://example.com/api/oauth/dialog
scopes:
write:pets: modify pets in your account
read:pets: read your pets
security:
- my_api_key: []

functions:
getData:
events:
- http:
documentation:
security:
- petstore_auth:
- write:pets
- read:pets
```

If you have specified an `security` at the document root, but this HTTP Request should not apply any security schemes, you should set security to be an array with an empty object:

```yml
custom:
documentation:
securitySchemes:
my_api_key:
type: apiKey
name: api_key
in: header
security:
- my_api_key: []

functions:
getData:
events:
- http:
documentation:
security:
- {}
```

#### `requestModels`

The `requestModels` property allows you to define models for the HTTP Request of the function event. You can define a different model for each different `Content-Type`. You can define a reference to the relevant request model named in the `models` section of your configuration (see [Defining Models](#models) section).
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-openapi-documenter",
"version": "0.0.32",
"version": "0.0.33",
"description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
"main": "index.js",
"keywords": [
Expand Down Expand Up @@ -42,7 +42,7 @@
},
"devDependencies": {
"chai": "^4.3.7",
"mocha": "^10.1.0",
"mocha": "^10.2.0",
"sinon": "^15.0.0"
}
}
153 changes: 153 additions & 0 deletions src/definitionGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ class DefinitionGenerator {

async parse() {
this.createInfo()

if (this.serverless.service.custom.documentation.securitySchemes) {
this.createSecuritySchemes(this.serverless.service.custom.documentation.securitySchemes)

if (this.serverless.service.custom.documentation.security) {
this.openAPI.security = this.serverless.service.custom.documentation.security
}
}

await this.createPaths()
.catch(err => {
throw err
Expand Down Expand Up @@ -265,6 +274,10 @@ class DefinitionGenerator {
obj.externalDocs = documentation.externalDocumentation
}

if (Object.keys(documentation).includes('security')) {
obj.security = documentation.security
}

if (Object.keys(documentation).includes('deprecated'))
obj.deprecated = documentation.deprecated

Expand Down Expand Up @@ -523,6 +536,146 @@ class DefinitionGenerator {
}
}

addToComponents(type, schema, name) {
const schemaObj = {
[name]: schema
}

if (this.openAPI?.components) {
if (this.openAPI.components[type]) {
Object.assign(this.openAPI.components[type], schemaObj)
} else {
Object.assign(this.openAPI.components, {[type]: schemaObj})
}
} else {
const components = {
components: {
[type]: schemaObj
}
}

Object.assign(this.openAPI, components)
}
}

createSecuritySchemes(securitySchemes) {
for (const scheme of Object.keys(securitySchemes)) {
const securityScheme = securitySchemes[scheme]
const schema = {}

if (securityScheme.description)
schema.description = securityScheme.description

switch(securityScheme.type.toLowerCase()) {
case 'apikey':
const apiKeyScheme = this.createAPIKeyScheme(securityScheme)
schema.type = 'apiKey'
Object.assign(schema, apiKeyScheme)
break;

case 'http':
const HTTPScheme = this.createHTTPScheme(securityScheme)
schema.type = 'http'
Object.assign(schema, HTTPScheme)
break;

case 'openidconnect':
const openIdConnectScheme = this.createOpenIDConnectScheme(securityScheme)
schema.type = 'openIdConnect'
Object.assign(schema, openIdConnectScheme)
break;

case 'oauth2':
const oAuth2Scheme = this.createOAuth2Scheme(securityScheme)
schema.type = 'oauth2'
Object.assign(schema, oAuth2Scheme)
break;
}

this.addToComponents('securitySchemes', schema, scheme)
}
}

createAPIKeyScheme(securitySchema) {
const schema = {}
if (securitySchema.name)
schema.name = securitySchema.name
else
throw new Error('Security Scheme for "apiKey" requires the name of the header, query or cookie parameter to be used')

if (securitySchema.in)
schema.in = securitySchema.in
else
throw new Error('Security Scheme for "apiKey" requires the location of the API key: header, query or cookie parameter')

return schema
}

createHTTPScheme(securitySchema) {
const schema = {}

if (securitySchema.scheme)
schema.scheme = securitySchema.scheme
else
throw new Error('Security Scheme for "http" requires scheme')

if (securitySchema.bearerFormat)
schema.bearerFormat = securitySchema.bearerFormat

return schema
}

createOpenIDConnectScheme(securitySchema) {
const schema = {}
if (securitySchema.openIdConnectUrl)
schema.openIdConnectUrl = securitySchema.openIdConnectUrl
else
throw new Error('Security Scheme for "openIdConnect" requires openIdConnectUrl')

return schema
}

createOAuth2Scheme(securitySchema) {
const schema = {}
if (securitySchema.flows) {
const flows = this.createOAuthFlows(securitySchema.flows)
Object.assign(schema, {flows: flows})
} else
throw new Error('Security Scheme for "oauth2" requires flows')

return schema
}

createOAuthFlows(flows) {
const obj = {}
for (const flow of Object.keys(flows)) {
const schema = {}
if (["implicit", 'authorizationCode'].includes(flow))
if (flows[flow].authorizationUrl)
schema.authorizationUrl = flows[flow].authorizationUrl
else
throw new Error(`oAuth2 ${flow} flow requires an authorizationUrl`)

if (['password', 'clientCredentials', 'authorizationCode'].includes(flow)) {
if (flows[flow].tokenUrl)
schema.tokenUrl = flows[flow].tokenUrl
else
throw new Error(`oAuth2 ${flow} flow requires a tokenUrl`)
}

if (flows[flow].refreshUrl)
schema.refreshUrl = flows[flow].refreshUrl

if (flows[flow].scopes)
schema.scopes = flows[flow].scopes
else
throw new Error(`oAuth2 ${flow} flow requires scopes`)

Object.assign(obj, {[flow]: schema})
}
return obj
}

createExamples(examples) {
const examplesObj = {}

Expand Down
Loading