这是indexloc提供的服务,不要输入任何密码
Skip to content
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
20 changes: 20 additions & 0 deletions autoconfig.go
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 1 deletion openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 changes: 77 additions & 14 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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)
}
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
97 changes: 97 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package huma

import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -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}",
},
})
}