diff --git a/internal/constraints/cache.go b/internal/constraints/cache.go index 7f56938..b546849 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -49,18 +49,25 @@ func (c *Cache) Build( fieldDesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, extensionTypeResolver protoregistry.ExtensionTypeResolver, + allowUnknownFields bool, forItems bool, ) (set expression.ProgramSet, err error) { constraints, done, err := c.resolveConstraints( fieldDesc, fieldConstraints, - extensionTypeResolver, forItems, ) if done { return nil, err } + if err = reparseUnrecognized(extensionTypeResolver, constraints); err != nil { + return nil, fmt.Errorf("error reparsing message: %w", err) + } + if !allowUnknownFields && len(constraints.GetUnknown()) > 0 { + return nil, errors.NewCompilationErrorf("unknown rules in %s", constraints.Descriptor().FullName()) + } + env, err = c.prepareEnvironment(env, fieldDesc, constraints, forItems) if err != nil { return nil, err @@ -103,7 +110,6 @@ func (c *Cache) Build( func (c *Cache) resolveConstraints( fieldDesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - extensionTypeResolver protoregistry.ExtensionTypeResolver, forItems bool, ) (rules protoreflect.Message, done bool, err error) { constraints := fieldConstraints.ProtoReflect() @@ -124,10 +130,6 @@ func (c *Cache) resolveConstraints( return nil, true, nil } rules = constraints.Get(setOneof).Message() - // Reparse unrecognized fields so that we get dynamic extensions. - if err = reparseUnrecognized(extensionTypeResolver, rules); err != nil { - return nil, false, fmt.Errorf("error reparsing message: %w", err) - } return rules, false, nil } diff --git a/internal/constraints/cache_test.go b/internal/constraints/cache_test.go index dc689b5..0c13412 100644 --- a/internal/constraints/cache_test.go +++ b/internal/constraints/cache_test.go @@ -101,7 +101,7 @@ func TestCache_BuildStandardConstraints(t *testing.T) { require.NoError(t, err) c := NewCache() - set, err := c.Build(env, test.desc, test.cons, protoregistry.GlobalTypes, test.forItems) + set, err := c.Build(env, test.desc, test.cons, protoregistry.GlobalTypes, false, test.forItems) if test.exErr { assert.Error(t, err) } else { diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index 82447c4..a795097 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -38,6 +38,7 @@ type Builder struct { constraints constraints.Cache resolver StandardConstraintResolver extensionTypeResolver protoregistry.ExtensionTypeResolver + allowUnknownFields bool Load func(desc protoreflect.MessageDescriptor) MessageEvaluator } @@ -53,6 +54,7 @@ func NewBuilder( disableLazy bool, res StandardConstraintResolver, extensionTypeResolver protoregistry.ExtensionTypeResolver, + allowUnknownFields bool, seedDesc ...protoreflect.MessageDescriptor, ) *Builder { bldr := &Builder{ @@ -60,6 +62,7 @@ func NewBuilder( constraints: constraints.NewCache(), resolver: res, extensionTypeResolver: extensionTypeResolver, + allowUnknownFields: allowUnknownFields, } if disableLazy { @@ -362,6 +365,7 @@ func (bldr *Builder) processStandardConstraints( fdesc, constraints, bldr.extensionTypeResolver, + bldr.allowUnknownFields, forItems, ) if err != nil { diff --git a/internal/evaluator/builder_test.go b/internal/evaluator/builder_test.go index 82573a7..bdee0cd 100644 --- a/internal/evaluator/builder_test.go +++ b/internal/evaluator/builder_test.go @@ -34,7 +34,7 @@ func TestBuildCache(t *testing.T) { env, err := celext.DefaultEnv(true) require.NoError(t, err, "failed to construct CEL environment") bldr := NewBuilder( - env, false, resolver.DefaultResolver{}, protoregistry.GlobalTypes, + env, false, resolver.DefaultResolver{}, protoregistry.GlobalTypes, false, ) wg := sync.WaitGroup{} for i := 0; i < 100; i++ { diff --git a/validator.go b/validator.go index 5618a7d..e02de71 100644 --- a/validator.go +++ b/validator.go @@ -79,6 +79,7 @@ func New(options ...ValidatorOption) (*Validator, error) { cfg.disableLazy, cfg.resolver, cfg.extensionTypeResolver, + cfg.allowUnknownFields, cfg.desc..., ) @@ -110,6 +111,7 @@ type config struct { desc []protoreflect.MessageDescriptor resolver StandardConstraintResolver extensionTypeResolver protoregistry.ExtensionTypeResolver + allowUnknownFields bool } // A ValidatorOption modifies the default configuration of a Validator. See the @@ -198,3 +200,16 @@ func WithExtensionTypeResolver(extensionTypeResolver protoregistry.ExtensionType c.extensionTypeResolver = extensionTypeResolver } } + +// WithAllowUnknownFields specifies if the presence of unknown field constraints +// should cause compilation to fail with an error. When set to false, an unknown +// field will simply be ignored, which will cause constraints to silently not be +// applied. This condition may occur if a predefined constraint definition isn't +// present in the extension type resolver, or when passing dynamic messages with +// standard constraints defined in a newer version of protovalidate. The default +// value is false, to prevent silently-incorrect validation from occurring. +func WithAllowUnknownFields(allowUnknownFields bool) ValidatorOption { + return func(c *config) { + c.allowUnknownFields = allowUnknownFields + } +}