feat: security & autoconfiguration

This commit is contained in:
Daniel G. Taylor 2020-12-22 10:00:43 -08:00
parent 3ddc01b24d
commit f9332efc1d
No known key found for this signature in database
GPG key ID: 8D100732CA686E06
4 changed files with 213 additions and 15 deletions

20
autoconfig.go Normal file
View file

@ -0,0 +1,20 @@
package huma
// AutoConfigVar represents a variable given by the user when prompted during
// auto-configuration setup of an API.
type AutoConfigVar struct {
Description string `json:"description,omitempty"`
Example string `json:"example,omitempty"`
Default interface{} `json:"default,omitempty"`
Enum []interface{} `json:"enum,omitempty"`
}
// AutoConfig holds an API's automatic configuration settings for the CLI. These
// are advertised via OpenAPI extension and picked up by the CLI to make it
// easier to get started using an API.
type AutoConfig struct {
Security string `json:"security"`
Headers map[string]string `json:"headers,omitempty"`
Prompt map[string]AutoConfigVar `json:"prompt,omitempty"`
Params map[string]string `json:"params"`
}

View file

@ -46,7 +46,8 @@ type oaParam struct {
}
type oaComponents struct {
Schemas map[string]*schema.Schema `json:"schemas,omitempty"`
Schemas map[string]*schema.Schema `json:"schemas,omitempty"`
SecuritySchemes map[string]oaSecurityScheme `json:"securitySchemes,omitempty"`
}
func (c *oaComponents) AddSchema(t reflect.Type, mode schema.Mode, hint string) string {
@ -100,3 +101,20 @@ func (c *oaComponents) AddSchema(t reflect.Type, mode schema.Mode, hint string)
return "#/components/schemas/" + name
}
type oaFlow struct {
AuthorizationURL string `json:"authorizationUrl,omitempty"`
TokenURL string `json:"tokenUrl,omitempty"`
Scopes map[string]string `json:"scopes,omitempty"`
}
type oaFlows struct {
ClientCredentials *oaFlow `json:"clientCredentials,omitempty"`
AuthorizationCode *oaFlow `json:"authorizationCode,omitempty"`
}
type oaSecurityScheme struct {
Type string `json:"type"`
Scheme string `json:"scheme,omitempty"`
Flows oaFlows `json:"flows,omitempty"`
}

View file

@ -33,13 +33,15 @@ type Router struct {
mux *chi.Mux
resources []*Resource
title string
version string
description string
contact oaContact
servers []oaServer
// securitySchemes
// security
title string
version string
description string
contact oaContact
servers []oaServer
securitySchemes map[string]oaSecurityScheme
security map[string][]string
autoConfig *AutoConfig
// Documentation handler function
docsPrefix string
@ -72,7 +74,8 @@ func (r *Router) OpenAPI() *gabs.Container {
}
components := &oaComponents{
Schemas: map[string]*schema.Schema{},
Schemas: map[string]*schema.Schema{},
SecuritySchemes: r.securitySchemes,
}
paths, _ := doc.Object("paths")
@ -82,6 +85,14 @@ func (r *Router) OpenAPI() *gabs.Container {
doc.Set(components, "components")
if len(r.security) > 0 {
doc.Set(r.security, "security")
}
if r.autoConfig != nil {
doc.Set(r.autoConfig, "x-cli-config")
}
if r.openapiHook != nil {
r.openapiHook(doc)
}
@ -104,6 +115,56 @@ func (r *Router) ServerLink(description, uri string) {
})
}
// GatewayBasicAuth documents that the API gateway handles auth using HTTP Basic.
func (r *Router) GatewayBasicAuth(name string) {
r.securitySchemes[name] = oaSecurityScheme{
Type: "http",
Scheme: "basic",
}
}
// GatewayClientCredentials documents that the API gateway handles auth using
// OAuth2 client credentials (pre-shared secret).
func (r *Router) GatewayClientCredentials(name, tokenURL string, scopes map[string]string) {
r.securitySchemes[name] = oaSecurityScheme{
Type: "oauth2",
Flows: oaFlows{
ClientCredentials: &oaFlow{
TokenURL: tokenURL,
Scopes: scopes,
},
},
}
}
// GatewayAuthCode documents that the API gateway handles auth using
// OAuth2 authorization code (user login).
func (r *Router) GatewayAuthCode(name, authorizeURL, tokenURL string, scopes map[string]string) {
r.securitySchemes[name] = oaSecurityScheme{
Type: "oauth2",
Flows: oaFlows{
AuthorizationCode: &oaFlow{
AuthorizationURL: authorizeURL,
TokenURL: tokenURL,
Scopes: scopes,
},
},
}
}
// AutoConfig sets up CLI autoconfiguration via `x-cli-config` for use by CLI
// clients, e.g. using a tool like Restish (https://rest.sh/).
func (r *Router) AutoConfig(autoConfig AutoConfig) {
r.autoConfig = &autoConfig
}
// SecurityRequirement sets up a security requirement for the entire API by
// name and with the given scopes. Use together with the other auth options
// like GatewayAuthCode.
func (r *Router) SecurityRequirement(name string, scopes ...string) {
r.security[name] = scopes
}
// Resource creates a new resource attached to this router at the given path.
// The path can include parameters, e.g. `/things/{thing-id}`. Each resource
// path must be unique.
@ -256,12 +317,14 @@ func New(docs, version string) *Router {
title, desc := splitDocs(docs)
r := &Router{
mux: chi.NewRouter(),
resources: []*Resource{},
title: title,
description: desc,
version: version,
servers: []oaServer{},
mux: chi.NewRouter(),
resources: []*Resource{},
title: title,
description: desc,
version: version,
servers: []oaServer{},
securitySchemes: map[string]oaSecurityScheme{},
security: map[string][]string{},
}
r.docsHandler = RapiDocHandler(r)

View file

@ -2,6 +2,7 @@ package huma
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
@ -240,3 +241,99 @@ func TestInvalidPathParam(t *testing.T) {
})
})
}
func TestRouterSecurity(t *testing.T) {
app := newTestRouter()
// Document that the API gateway handles auth via OAuth2 Authorization Code.
app.GatewayAuthCode("default", "https://example.com/authorize", "https://example.com/token", nil)
app.GatewayClientCredentials("m2m", "https://example.com/token", nil)
app.GatewayBasicAuth("basic")
// Every call must be authenticated using the default auth mechanism
// registered above.
app.SecurityRequirement("default")
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/openapi.json", nil)
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var parsed map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &parsed)
assert.Nil(t, err)
assert.Equal(t, parsed["security"], map[string]interface{}{
"default": nil,
})
assert.Equal(t, parsed["components"].(map[string]interface{})["securitySchemes"], map[string]interface{}{
"default": map[string]interface{}{
"type": "oauth2",
"flows": map[string]interface{}{
"authorizationCode": map[string]interface{}{
"authorizationUrl": "https://example.com/authorize",
"tokenUrl": "https://example.com/token",
},
},
},
"m2m": map[string]interface{}{
"type": "oauth2",
"flows": map[string]interface{}{
"clientCredentials": map[string]interface{}{
"tokenUrl": "https://example.com/token",
},
},
},
"basic": map[string]interface{}{
"type": "http",
"scheme": "basic",
"flows": map[string]interface{}{},
},
})
}
// TODO: test app.AutoConfig
func TestRouterAutoConfig(t *testing.T) {
app := newTestRouter()
app.GatewayAuthCode("authcode", "https://example.com/authorize", "https://example.com/token", nil)
app.SecurityRequirement("authcode")
app.AutoConfig(AutoConfig{
Security: "authcode",
Prompt: map[string]AutoConfigVar{
"extra": {
Description: "Some extra value",
Example: "abc123",
},
},
Params: map[string]string{
"another": "https://example.com/extras/{extra}",
},
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/openapi.json", nil)
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var parsed map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &parsed)
assert.Nil(t, err)
assert.Equal(t, parsed["x-cli-config"], map[string]interface{}{
"security": "authcode",
"prompt": map[string]interface{}{
"extra": map[string]interface{}{
"description": "Some extra value",
"example": "abc123",
},
},
"params": map[string]interface{}{
"another": "https://example.com/extras/{extra}",
},
})
}