Skip to content

Commit 687ef34

Browse files
feat(openapi3): ForbiddenFieldError cluster for set-but-not-allowed fields
Converts four 'field MUST NOT be set in this context' sites: - header.name (given by the headers map key) - header.in (implicitly 'header') - OAuth flow authorizationUrl (wrong flow type) - OAuth flow tokenUrl (wrong flow type)
1 parent d237575 commit 687ef34

5 files changed

Lines changed: 146 additions & 4 deletions

File tree

.github/docs/openapi3.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,24 @@ func (e *FieldVersionMismatchError) Error() string
727727

728728
func (e *FieldVersionMismatchError) Unwrap() error
729729

730+
type ForbiddenFieldError struct {
731+
// Field is the name of the forbidden field (e.g. "name", "in",
732+
// "authorizationUrl", "tokenUrl").
733+
Field string
734+
// Cause is the underlying leaf error. Walked by errors.Unwrap.
735+
Cause error
736+
// Origin is the source location of the offending element when the
737+
// document was loaded with Loader.IncludeOrigin = true.
738+
Origin *Origin
739+
}
740+
ForbiddenFieldError clusters "field X must not be set in this context"
741+
failures (header.name and header.in inside a Headers map, OAuth flow URLs
742+
that don't apply to the chosen flow type).
743+
744+
func (e *ForbiddenFieldError) Error() string
745+
746+
func (e *ForbiddenFieldError) Unwrap() error
747+
730748
type FormatValidator[T any] interface {
731749
Validate(value T) error
732750
}
@@ -769,6 +787,14 @@ func (header *Header) UnmarshalJSON(data []byte) error
769787
func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) error
770788
Validate returns an error if Header does not comply with the OpenAPI spec.
771789

790+
type HeaderInForbidden struct{ ValidationError }
791+
792+
func (e *HeaderInForbidden) As(target any) bool
793+
794+
type HeaderNameForbidden struct{ ValidationError }
795+
796+
func (e *HeaderNameForbidden) As(target any) bool
797+
772798
type HeaderRef struct {
773799
// Extensions only captures fields starting with 'x-' as no other fields
774800
// are allowed by the openapi spec.
@@ -1165,6 +1191,14 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e
11651191
Validate returns an error if OAuthFlows does not comply with the OpenAPI
11661192
spec.
11671193

1194+
type OAuthFlowAuthorizationURLForbidden struct{ ValidationError }
1195+
1196+
func (e *OAuthFlowAuthorizationURLForbidden) As(target any) bool
1197+
1198+
type OAuthFlowTokenURLForbidden struct{ ValidationError }
1199+
1200+
func (e *OAuthFlowTokenURLForbidden) As(target any) bool
1201+
11681202
type OAuthFlows struct {
11691203
Extensions map[string]any `json:"-" yaml:"-"`
11701204
Origin *Origin `json:"-" yaml:"-"`

openapi3/header.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er
5454
ctx = WithValidationOptions(ctx, opts...)
5555

5656
if header.Name != "" {
57-
return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map")
57+
return newHeaderNameForbidden(header.Origin)
5858
}
5959
if header.In != "" {
60-
return errors.New("header 'in' MUST NOT be specified, it is implicitly in header")
60+
return newHeaderInForbidden(header.Origin)
6161
}
6262

6363
// Validate a parameter's serialization method.

openapi3/security_scheme.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ...
404404
case flow.AuthorizationURL == "" && in:
405405
return errors.New("field 'authorizationUrl' is empty or missing")
406406
case flow.AuthorizationURL != "" && !in:
407-
return errors.New("field 'authorizationUrl' should not be set")
407+
return newOAuthFlowAuthorizationURLForbidden(flow.Origin)
408408
case flow.AuthorizationURL != "":
409409
if _, err := url.Parse(flow.AuthorizationURL); err != nil {
410410
return fmt.Errorf("field 'authorizationUrl' is invalid: %w", err)
@@ -417,7 +417,7 @@ func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ...
417417
case flow.TokenURL == "" && in:
418418
return errors.New("field 'tokenUrl' is empty or missing")
419419
case flow.TokenURL != "" && !in:
420-
return errors.New("field 'tokenUrl' should not be set")
420+
return newOAuthFlowTokenURLForbidden(flow.Origin)
421421
case flow.TokenURL != "":
422422
if _, err := url.Parse(flow.TokenURL); err != nil {
423423
return fmt.Errorf("field 'tokenUrl' is invalid: %w", err)

openapi3/validation_error.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,23 @@ type FieldVersionMismatchError struct {
155155
func (e *FieldVersionMismatchError) Error() string { return e.Cause.Error() }
156156
func (e *FieldVersionMismatchError) Unwrap() error { return e.Cause }
157157

158+
// ForbiddenFieldError clusters "field X must not be set in this
159+
// context" failures (header.name and header.in inside a Headers map,
160+
// OAuth flow URLs that don't apply to the chosen flow type).
161+
type ForbiddenFieldError struct {
162+
// Field is the name of the forbidden field (e.g. "name", "in",
163+
// "authorizationUrl", "tokenUrl").
164+
Field string
165+
// Cause is the underlying leaf error. Walked by errors.Unwrap.
166+
Cause error
167+
// Origin is the source location of the offending element when the
168+
// document was loaded with Loader.IncludeOrigin = true.
169+
Origin *Origin
170+
}
171+
172+
func (e *ForbiddenFieldError) Error() string { return e.Cause.Error() }
173+
func (e *ForbiddenFieldError) Unwrap() error { return e.Cause }
174+
158175
// ---------------------------------------------------------------------
159176
// Leaf types — one per call site. Each embeds ValidationError for
160177
// Error() and As-to-base, and is wrapped in its cluster type when
@@ -198,6 +215,32 @@ func (e *ServerURLRequired) As(target any) bool {
198215
return asValidationError(target, &e.ValidationError)
199216
}
200217

218+
// ForbiddenFieldError leaves.
219+
220+
type HeaderNameForbidden struct{ ValidationError }
221+
222+
func (e *HeaderNameForbidden) As(target any) bool {
223+
return asValidationError(target, &e.ValidationError)
224+
}
225+
226+
type HeaderInForbidden struct{ ValidationError }
227+
228+
func (e *HeaderInForbidden) As(target any) bool {
229+
return asValidationError(target, &e.ValidationError)
230+
}
231+
232+
type OAuthFlowAuthorizationURLForbidden struct{ ValidationError }
233+
234+
func (e *OAuthFlowAuthorizationURLForbidden) As(target any) bool {
235+
return asValidationError(target, &e.ValidationError)
236+
}
237+
238+
type OAuthFlowTokenURLForbidden struct{ ValidationError }
239+
240+
func (e *OAuthFlowTokenURLForbidden) As(target any) bool {
241+
return asValidationError(target, &e.ValidationError)
242+
}
243+
201244
// FieldVersionMismatchError leaves — non-schema fields.
202245

203246
type InfoSummaryFieldFor31Plus struct{ ValidationError }
@@ -414,6 +457,36 @@ func newServerURLRequired(origin *Origin) error {
414457
&ServerURLRequired{ValidationError{Message: "value of url must be a non-empty string"}}, origin)
415458
}
416459

460+
// newForbiddenField wraps leaf in a *ForbiddenFieldError carrying the
461+
// name of the field that the spec forbids in the current context.
462+
func newForbiddenField(field string, leaf error, origin *Origin) error {
463+
return &ForbiddenFieldError{Field: field, Cause: leaf, Origin: origin}
464+
}
465+
466+
func newHeaderNameForbidden(origin *Origin) error {
467+
const msg = "header 'name' MUST NOT be specified, it is given in the corresponding headers map"
468+
return newForbiddenField("name",
469+
&HeaderNameForbidden{ValidationError{Message: msg}}, origin)
470+
}
471+
472+
func newHeaderInForbidden(origin *Origin) error {
473+
const msg = "header 'in' MUST NOT be specified, it is implicitly in header"
474+
return newForbiddenField("in",
475+
&HeaderInForbidden{ValidationError{Message: msg}}, origin)
476+
}
477+
478+
func newOAuthFlowAuthorizationURLForbidden(origin *Origin) error {
479+
const msg = "field 'authorizationUrl' should not be set"
480+
return newForbiddenField("authorizationUrl",
481+
&OAuthFlowAuthorizationURLForbidden{ValidationError{Message: msg}}, origin)
482+
}
483+
484+
func newOAuthFlowTokenURLForbidden(origin *Origin) error {
485+
const msg = "field 'tokenUrl' should not be set"
486+
return newForbiddenField("tokenUrl",
487+
&OAuthFlowTokenURLForbidden{ValidationError{Message: msg}}, origin)
488+
}
489+
417490
// newSchemaValueError wraps the result of schema.VisitJSON in a
418491
// *SchemaValueError cluster, identifying which schema sub-field
419492
// (example, default, ...) carried the offending value. cause is

openapi3/validation_error_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,38 @@ func TestValidationError_FlowsThroughMultiError(t *testing.T) {
429429
var ve *openapi3.ValidationError
430430
require.True(t, errors.As(me, &ve))
431431
}
432+
433+
// Pin ForbiddenFieldError cluster + leaf reachability for the four
434+
// sites where a field is set but the spec forbids it in that context.
435+
func TestValidationError_ForbiddenFieldLeaves(t *testing.T) {
436+
t.Run("header.name forbidden", func(t *testing.T) {
437+
h := &openapi3.Header{Parameter: openapi3.Parameter{Name: "X-Trace"}}
438+
err := h.Validate(context.Background())
439+
require.EqualError(t, err,
440+
"header 'name' MUST NOT be specified, it is given in the corresponding headers map")
441+
442+
var ffe *openapi3.ForbiddenFieldError
443+
require.True(t, errors.As(err, &ffe))
444+
require.Equal(t, "name", ffe.Field)
445+
446+
var leaf *openapi3.HeaderNameForbidden
447+
require.True(t, errors.As(err, &leaf))
448+
449+
var ve *openapi3.ValidationError
450+
require.True(t, errors.As(err, &ve))
451+
})
452+
453+
t.Run("header.in forbidden", func(t *testing.T) {
454+
h := &openapi3.Header{Parameter: openapi3.Parameter{In: "header"}}
455+
err := h.Validate(context.Background())
456+
require.EqualError(t, err,
457+
"header 'in' MUST NOT be specified, it is implicitly in header")
458+
459+
var ffe *openapi3.ForbiddenFieldError
460+
require.True(t, errors.As(err, &ffe))
461+
require.Equal(t, "in", ffe.Field)
462+
463+
var leaf *openapi3.HeaderInForbidden
464+
require.True(t, errors.As(err, &leaf))
465+
})
466+
}

0 commit comments

Comments
 (0)