mirror of
https://github.com/Fishwaldo/huma.git
synced 2025-03-15 19:31:27 +00:00
feat: security & autoconfiguration
This commit is contained in:
parent
3ddc01b24d
commit
f9332efc1d
4 changed files with 213 additions and 15 deletions
20
autoconfig.go
Normal file
20
autoconfig.go
Normal 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"`
|
||||
}
|
20
openapi.go
20
openapi.go
|
@ -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"`
|
||||
}
|
||||
|
|
91
router.go
91
router.go
|
@ -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)
|
||||
|
|
|
@ -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}",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue