Skip to content

Commit

Permalink
feat: support for OpenAPI security spec
Browse files Browse the repository at this point in the history
Co-authored-by: Gregor Casar <gregorcasar@gmail.com>
  • Loading branch information
2 people authored and wI2L committed Dec 22, 2021
1 parent 8a048be commit 7606e1d
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 37 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ coverage.txt

# GoLand
.idea

# VSCode
.history
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ fizz.Header(name, desc string, model interface{})
// Override the binding model of the operation.
fizz.InputModel(model interface{})

// Overrides the top-level security requirement of an operation.
// Note that this function can be used more than once to add several requirements.
fizz.Security(security *openapi.SecurityRequirement)

// Add an empty security requirement to this operation to make other security requirements optional.
fizz.WithOptionalSecurity()

// Remove any top-level security requirements for this operation.
fizz.WithoutSecurity()

// Add a Code Sample to the operation.
fizz.XCodeSample(codeSample *XCodeSample)
```
Expand Down Expand Up @@ -319,6 +329,7 @@ Fizz supports some native and imported types. A schema with a proper type and fo
* [`time.Duration`](https://golang.org/pkg/time/#Duration)
* [`net.URL`](https://golang.org/pkg/net/url/#URL)
* [`net.IP`](https://golang.org/pkg/net/#IP)

Note that, according to the doc, the inherent version of the address is a semantic property, and thus cannot be determined by Fizz. Therefore, the format returned is simply `ip`. If you want to specify the version, you can use the tags `format:"ipv4"` or `format:"ipv6"`.
* [`uuid.UUID`](https://godoc.org/github.com/gofrs/uuid#UUID)

Expand Down
23 changes: 23 additions & 0 deletions fizz.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,29 @@ func XCodeSample(cs *openapi.XCodeSample) func(*openapi.OperationInfo) {
}
}

// Overrides top-level security requirement for this operation.
// Note that this function can be used more than once to add several requirements.
func Security(security *openapi.SecurityRequirement) func(*openapi.OperationInfo) {
return func(o *openapi.OperationInfo) {
o.Security = append(o.Security, security)
}
}

// Add an empty security requirement to this operation to make other security requirements optional.
func WithOptionalSecurity() func(*openapi.OperationInfo) {
return func(o *openapi.OperationInfo) {
var emptyRequirement openapi.SecurityRequirement = make(openapi.SecurityRequirement)
o.Security = append(o.Security, &emptyRequirement)
}
}

// Remove any top-level security requirements for this operation.
func WithoutSecurity() func(*openapi.OperationInfo) {
return func(o *openapi.OperationInfo) {
o.Security = []*openapi.SecurityRequirement{}
}
}

// OperationFromContext returns the OpenAPI operation from
// the givent Gin context or an error if none is found.
func OperationFromContext(c *gin.Context) (*openapi.Operation, error) {
Expand Down
40 changes: 37 additions & 3 deletions fizz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ func TestSpecHandler(t *testing.T) {
Header("X-Request-Id", "Unique request ID", String),
// Additional responses.
Response("429", "", String, []*openapi.ResponseHeader{
&openapi.ResponseHeader{
{
Name: "X-Rate-Limit",
Description: "Rate limit",
Model: Integer,
Expand All @@ -271,6 +271,8 @@ func TestSpecHandler(t *testing.T) {
Label: "v4.4",
Source: "curl http://0.0.0.0:8080",
}),
// Explicit override for SecurityRequirement (allow-all)
WithoutSecurity(),
},
tonic.Handler(func(c *gin.Context) error {
return nil
Expand All @@ -280,6 +282,8 @@ func TestSpecHandler(t *testing.T) {
fizz.GET("/test/:a/:b", []OperationOption{
ID("GetTest2"),
InputModel(&testInputModel{}),
WithOptionalSecurity(),
Security(&openapi.SecurityRequirement{"oauth2": []string{"write:pets", "read:pets"}}),
}, tonic.Handler(func(c *gin.Context) error {
return nil
}, 200))
Expand All @@ -301,11 +305,11 @@ func TestSpecHandler(t *testing.T) {
)

servers := []*openapi.Server{
&openapi.Server{
{
URL: "https://foo.bar/{basePath}",
Description: "Such Server, Very Wow",
Variables: map[string]*openapi.ServerVariable{
"basePath": &openapi.ServerVariable{
"basePath": {
Default: "v2",
Description: "version of the API",
Enum: []string{"v1", "v2", "beta"},
Expand All @@ -315,6 +319,36 @@ func TestSpecHandler(t *testing.T) {
}
fizz.Generator().SetServers(servers)

security := openapi.SecurityRequirement{
"api_key": []string{},
"oauth2": []string{"write:pets", "read:pets"},
}
fizz.Generator().SetSecurityRequirement(&security)

fizz.Generator().API().Components.SecuritySchemes = map[string]*openapi.SecuritySchemeOrRef{
"api_key": {
SecurityScheme: &openapi.SecurityScheme{
Type: "apiKey",
Name: "api_key",
In: "header",
},
},
"oauth2": {
SecurityScheme: &openapi.SecurityScheme{
Type: "oauth2",
Flows: &openapi.OAuthFlows{
Implicit: &openapi.OAuthFlow{
AuthorizationURL: "https://example.com/api/oauth/dialog",
Scopes: map[string]string{
"write:pets": "modify pets in your account",
"read:pets": "read your pets",
},
},
},
},
},
}

fizz.GET("/openapi.json", nil, fizz.OpenAPI(infos, "")) // default is JSON
fizz.GET("/openapi.yaml", nil, fizz.OpenAPI(infos, "yaml"))

Expand Down
7 changes: 7 additions & 0 deletions openapi/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ func (g *Generator) SetServers(servers []*Server) {
g.api.Servers = servers
}

// SetSecurityRequirement sets the security options for the
// current specification.
func (g *Generator) SetSecurityRequirement(security *SecurityRequirement) {
g.api.Security = security
}

// API returns a copy of the internal OpenAPI object.
func (g *Generator) API() *OpenAPI {
cpy := *g.api
Expand Down Expand Up @@ -251,6 +257,7 @@ func (g *Generator) AddOperation(path, method, tag string, in, out reflect.Type,
op.Deprecated = info.Deprecated
op.Responses = make(Responses)
op.XCodeSamples = info.XCodeSamples
op.Security = info.Security
}
if tag != "" {
op.Tags = append(op.Tags, tag)
Expand Down
1 change: 1 addition & 0 deletions openapi/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type OperationInfo struct {
Deprecated bool
InputModel interface{}
Responses []*OperationResponse
Security []*SecurityRequirement
XCodeSamples []*XCodeSample
}

Expand Down
127 changes: 112 additions & 15 deletions openapi/spec.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
package openapi

import "encoding/json"

// OpenAPI represents the root document object of
// an OpenAPI document.
type OpenAPI struct {
OpenAPI string `json:"openapi" yaml:"openapi"`
Info *Info `json:"info" yaml:"info"`
Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"`
Paths Paths `json:"paths" yaml:"paths"`
Components *Components `json:"components,omitempty" yaml:"components,omitempty"`
Tags []*Tag `json:"tags,omitempty" yaml:"tags,omitempty"`
XTagGroups []*XTagGroup `json:"x-tagGroups,omitempty" yaml:"x-tagGroups,omitempty"`
OpenAPI string `json:"openapi" yaml:"openapi"`
Info *Info `json:"info" yaml:"info"`
Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"`
Paths Paths `json:"paths" yaml:"paths"`
Components *Components `json:"components,omitempty" yaml:"components,omitempty"`
Tags []*Tag `json:"tags,omitempty" yaml:"tags,omitempty"`
Security *SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"`
}

// Components holds a set of reusable objects for different
// ascpects of the specification.
type Components struct {
Schemas map[string]*SchemaOrRef `json:"schemas,omitempty" yaml:"schemas,omitempty"`
Responses map[string]*ResponseOrRef `json:"responses,omitempty" yaml:"responses,omitempty"`
Parameters map[string]*ParameterOrRef `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Examples map[string]*ExampleOrRef `json:"examples,omitempty" yaml:"examples,omitempty"`
Headers map[string]*HeaderOrRef `json:"headers,omitempty" yaml:"headers,omitempty"`
Schemas map[string]*SchemaOrRef `json:"schemas,omitempty" yaml:"schemas,omitempty"`
Responses map[string]*ResponseOrRef `json:"responses,omitempty" yaml:"responses,omitempty"`
Parameters map[string]*ParameterOrRef `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Examples map[string]*ExampleOrRef `json:"examples,omitempty" yaml:"examples,omitempty"`
Headers map[string]*HeaderOrRef `json:"headers,omitempty" yaml:"headers,omitempty"`
SecuritySchemes map[string]*SecuritySchemeOrRef `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"`
}

// Info represents the metadata of an API.
Expand Down Expand Up @@ -184,6 +187,22 @@ type Schema struct {

// Operation describes an API operation on a path.
type Operation struct {
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ID string `json:"operationId,omitempty" yaml:"operationId,omitempty"`
Parameters []*ParameterOrRef `json:"parameters,omitempty" yaml:"parameters,omitempty"`
RequestBody *RequestBody `json:"requestBody,omitempty" yaml:"requestBody,omitempty"`
Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"`
Security []*SecurityRequirement `json:"security" yaml:"security"`
XCodeSamples []*XCodeSample `json:"x-codeSamples,omitempty" yaml:"x-codeSamples,omitempty"`
}

// A workaround for missing omitnil functionality.
// Explicitely omit the Security field from marshaling when it is nil, but not when empty.
type operationNilOmitted struct {
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Expand All @@ -196,6 +215,38 @@ type Operation struct {
XCodeSamples []*XCodeSample `json:"x-codeSamples,omitempty" yaml:"x-codeSamples,omitempty"`
}

// MarshalYAML implements yaml.Marshaler for Operation.
// Needed to marshall empty but non-null SecurityRequirements.
func (o *Operation) MarshalYAML() (interface{}, error) {
if o.Security == nil {
return omitOperationNilFields(o), nil
}
return o, nil
}

// MarshalJSON excludes empty but non-null SecurityRequirements.
func (o *Operation) MarshalJSON() ([]byte, error) {
if o.Security == nil {
return json.Marshal(omitOperationNilFields(o))
}
return json.Marshal(*o)
}

func omitOperationNilFields(o *Operation) *operationNilOmitted {
return &operationNilOmitted{
Tags: o.Tags,
Summary: o.Summary,
Description: o.Description,
ID: o.ID,
Parameters: o.Parameters,
RequestBody: o.RequestBody,
Responses: o.Responses,
Deprecated: o.Deprecated,
Servers: o.Servers,
XCodeSamples: o.XCodeSamples,
}
}

// Responses represents a container for the expected responses
// of an opration. It maps a HTTP response code to the expected
// response.
Expand Down Expand Up @@ -309,7 +360,53 @@ type Tag struct {
Description string `json:"description,omitempty" yaml:"description,omitempty"`
}

// XLogo represents the information about the x-logo extension
// SecuritySchemeOrRef represents a SecurityScheme that can be inlined
// or referenced in the API description.
type SecuritySchemeOrRef struct {
*SecurityScheme
*Reference
}

// MarshalYAML implements yaml.Marshaler for SecuritySchemeOrRef.
func (sor *SecuritySchemeOrRef) MarshalYAML() (interface{}, error) {
if sor.SecurityScheme != nil {
return sor.SecurityScheme, nil
}
return sor.Reference, nil
}

// SecurityScheme represents a security scheme that can be used by an operation.
type SecurityScheme struct {
Type string `json:"type,omitempty" yaml:"type,omitempty"`
Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"`
BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
In string `json:"in,omitempty" yaml:"in,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
OpenIDConnectURL string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"`
Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"`
}

// OAuthFlows represents all the supported OAuth flows.
type OAuthFlows struct {
Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"`
Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"`
ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"`
AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"`
}

// OAuthFlow represents an OAuth security scheme.
type OAuthFlow struct {
AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"`
}

// SecurityRequirement represents the security object in the API specification.
type SecurityRequirement map[string][]string

// XLogo represents the information about the x-logo extension.
// See: https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-logo
type XLogo struct {
URL string `json:"url,omitempty" yaml:"url,omitempty"`
Expand All @@ -318,14 +415,14 @@ type XLogo struct {
Href string `json:"href,omitempty" yaml:"href,omitempty"`
}

// XTagGroup represents the information about the x-tagGroups extension
// XTagGroup represents the information about the x-tagGroups extension.
// See: https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-taggroups
type XTagGroup struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
}

// XCodeSample represents the information about the x-codeSample extension
// XCodeSample represents the information about the x-codeSample extension.
// See: https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-codesamples
type XCodeSample struct {
Lang string `json:"lang,omitempty" yaml:"lang,omitempty"`
Expand Down
Loading

0 comments on commit 7606e1d

Please sign in to comment.