feat(openapi): allow route-level security overrides
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
ebad4c397d
commit
408a709a43
3 changed files with 87 additions and 3 deletions
3
group.go
3
group.go
|
|
@ -39,6 +39,9 @@ type RouteDescription struct {
|
|||
Summary string // Short summary
|
||||
Description string // Long description
|
||||
Tags []string // OpenAPI tags for grouping
|
||||
// Security overrides the default bearerAuth requirement when non-nil.
|
||||
// Use an empty, non-nil slice to mark the route as public.
|
||||
Security []map[string][]string
|
||||
Parameters []ParameterDescription
|
||||
RequestBody map[string]any // JSON Schema for request body (nil for GET)
|
||||
Response map[string]any // JSON Schema for success response data
|
||||
|
|
|
|||
10
openapi.go
10
openapi.go
|
|
@ -123,12 +123,16 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
"summary": rd.Summary,
|
||||
"description": rd.Description,
|
||||
"operationId": operationID(method, fullPath, operationIDs),
|
||||
"security": []any{
|
||||
"responses": operationResponses(method, rd.Response),
|
||||
}
|
||||
if rd.Security != nil {
|
||||
operation["security"] = rd.Security
|
||||
} else {
|
||||
operation["security"] = []any{
|
||||
map[string]any{
|
||||
"bearerAuth": []any{},
|
||||
},
|
||||
},
|
||||
"responses": operationResponses(method, rd.Response),
|
||||
}
|
||||
}
|
||||
if tags := resolvedOperationTags(g, rd); len(tags) > 0 {
|
||||
operation["tags"] = tags
|
||||
|
|
|
|||
|
|
@ -321,6 +321,83 @@ func TestSpecBuilder_Good_SecuredResponses(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_RouteSecurityOverrides(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
group := &specStubGroup{
|
||||
name: "security",
|
||||
basePath: "/api",
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/public",
|
||||
Summary: "Public endpoint",
|
||||
Security: []map[string][]string{},
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/scoped",
|
||||
Summary: "Scoped endpoint",
|
||||
Security: []map[string][]string{
|
||||
{
|
||||
"bearerAuth": []string{},
|
||||
},
|
||||
{
|
||||
"oauth2": []string{"read:items"},
|
||||
},
|
||||
},
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := sb.Build([]api.RouteGroup{group})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var spec map[string]any
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
paths := spec["paths"].(map[string]any)
|
||||
|
||||
publicOp := paths["/api/public"].(map[string]any)["get"].(map[string]any)
|
||||
publicSecurity, ok := publicOp["security"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected public security array, got %T", publicOp["security"])
|
||||
}
|
||||
if len(publicSecurity) != 0 {
|
||||
t.Fatalf("expected public route to have empty security requirement, got %v", publicSecurity)
|
||||
}
|
||||
|
||||
scopedOp := paths["/api/scoped"].(map[string]any)["get"].(map[string]any)
|
||||
scopedSecurity, ok := scopedOp["security"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected scoped security array, got %T", scopedOp["security"])
|
||||
}
|
||||
if len(scopedSecurity) != 2 {
|
||||
t.Fatalf("expected 2 security requirements, got %d", len(scopedSecurity))
|
||||
}
|
||||
firstReq := scopedSecurity[0].(map[string]any)
|
||||
if _, ok := firstReq["bearerAuth"]; !ok {
|
||||
t.Fatalf("expected bearerAuth requirement, got %v", firstReq)
|
||||
}
|
||||
secondReq := scopedSecurity[1].(map[string]any)
|
||||
if scopes, ok := secondReq["oauth2"].([]any); !ok || len(scopes) != 1 || scopes[0] != "read:items" {
|
||||
t.Fatalf("expected oauth2 read:items requirement, got %v", secondReq["oauth2"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_EnvelopeWrapping(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue