Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenAPI security (rebase) #59

Merged
merged 1 commit into from
Dec 22, 2021
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
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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we're including security-related helpers functions such as WithoutSecurity() and OptionalSecurity(), they should also be documented/mentionned.

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