fix(api): deep-clone swagger security schemes
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
69dd16cba6
commit
b0549dc14e
4 changed files with 111 additions and 3 deletions
|
|
@ -721,7 +721,7 @@ func securitySchemeComponents(overrides map[string]any) map[string]any {
|
|||
if name == "" || scheme == nil {
|
||||
continue
|
||||
}
|
||||
schemes[name] = scheme
|
||||
schemes[name] = cloneOpenAPIValue(scheme)
|
||||
}
|
||||
|
||||
return schemes
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ func WithSwaggerSecuritySchemes(schemes map[string]any) Option {
|
|||
if name == "" || scheme == nil {
|
||||
continue
|
||||
}
|
||||
e.swaggerSecuritySchemes[name] = scheme
|
||||
e.swaggerSecuritySchemes[name] = cloneOpenAPIValue(scheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
)
|
||||
|
||||
|
|
@ -52,7 +53,61 @@ func cloneSecuritySchemes(schemes map[string]any) map[string]any {
|
|||
|
||||
out := make(map[string]any, len(schemes))
|
||||
for name, scheme := range schemes {
|
||||
out[name] = scheme
|
||||
out[name] = cloneOpenAPIValue(scheme)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// cloneOpenAPIValue recursively copies JSON-like OpenAPI values so callers can
|
||||
// safely retain and reuse their original maps after configuring an engine.
|
||||
func cloneOpenAPIValue(v any) any {
|
||||
switch value := v.(type) {
|
||||
case map[string]any:
|
||||
out := make(map[string]any, len(value))
|
||||
for k, nested := range value {
|
||||
out[k] = cloneOpenAPIValue(nested)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, len(value))
|
||||
for i, nested := range value {
|
||||
out[i] = cloneOpenAPIValue(nested)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
rv := reflect.ValueOf(v)
|
||||
if !rv.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Map:
|
||||
out := reflect.MakeMapWithSize(rv.Type(), rv.Len())
|
||||
for _, key := range rv.MapKeys() {
|
||||
cloned := cloneOpenAPIValue(rv.MapIndex(key).Interface())
|
||||
if cloned == nil {
|
||||
out.SetMapIndex(key, reflect.Zero(rv.Type().Elem()))
|
||||
continue
|
||||
}
|
||||
out.SetMapIndex(key, reflect.ValueOf(cloned))
|
||||
}
|
||||
return out.Interface()
|
||||
case reflect.Slice:
|
||||
if rv.IsNil() {
|
||||
return v
|
||||
}
|
||||
out := reflect.MakeSlice(rv.Type(), rv.Len(), rv.Len())
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
cloned := cloneOpenAPIValue(rv.Index(i).Interface())
|
||||
if cloned == nil {
|
||||
out.Index(i).Set(reflect.Zero(rv.Type().Elem()))
|
||||
continue
|
||||
}
|
||||
out.Index(i).Set(reflect.ValueOf(cloned))
|
||||
}
|
||||
return out.Interface()
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,3 +312,56 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesConfiguredSSEPathWithoutBroker(t *
|
|||
t.Fatalf("expected x-sse-path=/events, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_Good_OpenAPISpecBuilderClonesSecuritySchemes(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
securityScheme := map[string]any{
|
||||
"type": "oauth2",
|
||||
"flows": map[string]any{
|
||||
"clientCredentials": map[string]any{
|
||||
"tokenUrl": "https://auth.example.com/token",
|
||||
},
|
||||
},
|
||||
}
|
||||
schemes := map[string]any{
|
||||
"oauth2": securityScheme,
|
||||
}
|
||||
|
||||
e, err := api.New(
|
||||
api.WithSwagger("Engine API", "Engine metadata", "2.0.0"),
|
||||
api.WithSwaggerSecuritySchemes(schemes),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Mutate the original input after configuration. The builder snapshot should
|
||||
// remain stable and keep the original token URL.
|
||||
securityScheme["type"] = "mutated"
|
||||
securityScheme["flows"].(map[string]any)["clientCredentials"].(map[string]any)["tokenUrl"] = "https://mutated.example.com/token"
|
||||
|
||||
data, err := e.OpenAPISpecBuilder().Build(nil)
|
||||
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)
|
||||
}
|
||||
|
||||
securitySchemes := spec["components"].(map[string]any)["securitySchemes"].(map[string]any)
|
||||
oauth2, ok := securitySchemes["oauth2"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("expected oauth2 security scheme in generated spec")
|
||||
}
|
||||
if oauth2["type"] != "oauth2" {
|
||||
t.Fatalf("expected cloned oauth2.type=oauth2, got %v", oauth2["type"])
|
||||
}
|
||||
flows := oauth2["flows"].(map[string]any)
|
||||
clientCredentials := flows["clientCredentials"].(map[string]any)
|
||||
if clientCredentials["tokenUrl"] != "https://auth.example.com/token" {
|
||||
t.Fatalf("expected original tokenUrl to be preserved, got %v", clientCredentials["tokenUrl"])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue