Module: Ruby::Rego::Builtins::Codecs::JsonSchema
- Defined in:
- lib/ruby/rego/builtins/codecs/json_schema.rb,
lib/ruby/rego/builtins/codecs/json_schema_email.rb,
lib/ruby/rego/builtins/codecs/json_schema_match.rb,
lib/ruby/rego/builtins/codecs/json_schema_formats.rb
Overview
The format assertions OPA 1.17 enforces, ported from gojsonschema’s format_checkers.go. A format
only constrains string instances; a value matches (true) when it conforms OR the format is one OPA
does not enforce (idn-hostname, duration, unknown names — annotation-only). The boolean is byte-exact
with OPA; rules mirror gojsonschema exactly:
- The lexical regexes are anchored \A..\z. gojsonschema applies Go
regexpwith^..$, which in RE2 (default flags) match whole-text — so a leading/trailing/embedded newline is rejected, unlike Ruby’s line-oriented ^..$. (json-pointer still accepts an embedded newline because its [^~/] class includes it, in both engines.) - date/time use Go’s time.Parse semantics, not the Times::GoLayout port (which rejects time-only and year-0 values): a strict structural regex plus a proleptic-Gregorian calendar check (Date::GREGORIAN — Ruby’s default Date is Julian before 1582) and hour/min/sec range checks.
- ipv4/ipv6 mirror Go net.ParseIP + a “.”/”:” check: IPAddr matches it exactly once zone (%) and CIDR (/) suffixes — which IPAddr accepts but net.ParseIP does not — are rejected.
- regex reuses the RE2 compile gate (JsonSchema.re2_valid?), the same engine as the pattern keyword.
- uri/uri-reference (and their iri aliases) and uri-template reuse Uri::Parser, the gem’s port of Go net/url.Parse: a value matches when it parses, has no backslash (gojsonschema’s explicit reject), and — for uri/iri — a non-empty scheme; uri-template additionally matches the parsed path against gojsonschema’s template regex via the re2 engine (Go’s). iri == uri exactly because url.Parse already accepts unicode hosts/paths, so no separate IDN logic is needed.
- email/idn-email run Go’s net/mail.ParseAddress (a full RFC 5322 address parse, not a “valid email” regex) via the MailAddress port (json_schema_email). idn-email is the SAME checker.
Implemented: the lexical / date-time / net / regex formats, the uri family, and email/idn-email. The remaining names (idn-hostname, duration, unknown) fall through to annotation-only (true).
Defined Under Namespace
Modules: Formats Classes: Matcher
Constant Summary collapse
- SCHEMA_TYPES =
The seven JSON Schema primitive type names a
typekeyword may name. %w[null boolean object array number integer string].freeze
- SUBSCHEMA_KEYWORDS =
Keywords whose value is a single subschema (gojsonschema also accepts a boolean for the two
additional*ones; that is handled in subschema_error). %w[additionalProperties additionalItems propertyNames not if then else contains].freeze
- SUBSCHEMA_MAP_KEYWORDS =
Keywords whose value is an object mapping names to subschemas.
$defsis deliberately absent: xeipuuv targets draft-04/06/07, so it never validates the draft-2019$defs(an unknown keyword), and a malformed$defssubschema therefore does not make the schema invalid. %w[properties patternProperties definitions].freeze
- SUBSCHEMA_LIST_KEYWORDS =
Keywords whose value is an array of subschemas (an empty array is accepted).
%w[allOf anyOf oneOf].freeze
- NUMBER_KEYWORDS =
Keywords whose value must be a JSON number (any number, including negative/zero).
%w[minimum maximum].freeze
- NON_NEGATIVE_INT_KEYWORDS =
Keywords whose value must be a non-negative integer.
%w[minLength maxLength minItems maxItems minProperties maxProperties].freeze
- GO_REJECTED_ESCAPE =
A bare
\Cescape (preceded by an even run of backslashes, so its\is a real escape, not inside a\\literal). C++ RE2 accepts\C(match-any-byte); Go’sregexp— what gojsonschema uses — rejects it, so it is filtered out after the RE2 compile check. The*+possessive quantifier makes the backslash-run scan non-backtracking (linear, ReDoS-safe on untrusted input). /(?<!\\)(?:\\\\)*+\\C/- JSON_STRING_TOKEN =
One JSON string literal, for the comment scan below (quote, escapes-or-non-quote bytes, quote).
/"(?:\\.|[^"\\])*"/- RE2_MAX_MEM =
Two bounds on compiling an untrusted regex (
pattern/patternPropertiesand theformat: "regex"assertion). The re2 gem’s C++ RE2 is far slower than Go’sregexp(which OPA uses) on adversarial patterns, a CPU denial-of-service the default 8 MB budget does not bound; Go compiles the same inputs in milliseconds. Both bounds trade byte-exact fidelity for safety on these inputs (a pattern they reject may be one OPA accepts — a documented divergence):- RE2_MAX_MEM caps the COMPILE phase. Nested counted repetition compiles ~quadratically, and the
peak compile wall-clock scales with the program budget (256 KB still allowed a ~130 ms single
pattern, and N such patterns in one schema amplified linearly). 64 KB collapses that peak to
<1 ms while still accepting every realistic pattern (emails, dates,
a{1000},(ab){500}, …) — measured: nothing in the realistic set or the existing golden corpus is rejected at 64 KB. - RE2_MAX_PATTERN_BYTES caps the PARSE phase, which max_mem does NOT bound: expanding unicode
property classes (
\p{L}) is linear in pattern length and runs to completion before the memory check, so a ~1 MB class-dense pattern still takes seconds. 4 KB bounds that to ~tens of ms and is 4× any realistic JSON-Schema pattern (the existing pattern golden corpus is well under it).
Both bounds live in re2_compile, so every compile path — verify (re2_compatible?), the match-time
pattern/patternPropertiescheck (the Matcher’s cached re2_matches?), and the format:regex gate (re2_valid?) — shares the same safety. match_schema also memoises each distinct pattern per call so patternProperties (every pattern × every property name) compiles N patterns, not N×M times. - RE2_MAX_MEM caps the COMPILE phase. Nested counted repetition compiles ~quadratically, and the
peak compile wall-clock scales with the program budget (256 KB still allowed a ~130 ms single
pattern, and N such patterns in one schema amplified linearly). 64 KB collapses that peak to
<1 ms while still accepting every realistic pattern (emails, dates,
64 * 1024
- RE2_MAX_PATTERN_BYTES =
4 * 1024
- MAX_SCHEMA_DEPTH =
Bound on schema-recursion depth (nesting and $ref-chain length). A flat
definitionsof N chained $refs has no structural depth, so neither JSON.parse’s max_nesting (100) nor Value.from_ruby’s marshaling ceiling caps it — without this guard a long chain would recurse to a SystemStackError that aborts the whole policy (only BuiltinArgumentError is rescued). 100 matches the gem’s other JSON-nesting limits; a schema nested/ref-chained past it is reported invalid (gojsonschema accepts deeper before it too stack-overflows — a documented divergence, not a crash). 100- SCHEMA_TOO_DEEP =
Sentinel thrown past the verify recursion and caught in valid_schema.
:__verify_schema_too_deep__- MATCH_ERROR =
The best-effort error returned when a document does not match (gojsonschema returns one richly typed object per failure; only the array’s non-emptiness is contractual).
{ "desc" => "document does not match schema", "error" => "(root): does not match", "field" => "(root)", "type" => "match" }.freeze
Class Method Summary collapse
-
.match(document_value, schema_value) ⇒ Array(bool, Array), Symbol
:reek:NilCheck :reek:TooManyStatements.
-
.re2_compile(pattern) ⇒ Object
The capped RE2 compilation of
pattern, or nil if it is unusable (non-string, invalid encoding, or over the byte cap). -
.re2_valid?(value) ⇒ Boolean
Whether
valueis a regex Go’sregexp(RE2) would compile — theformat: "regex"assertion, which gojsonschema implements with the sameregexp.Compileas thepatternkeyword. -
.resolve_ref_target(ref, root) ⇒ Object
The schema node a
$refresolves to (for match_schema’s document validation), or nil. -
.valid_schema(schema) ⇒ Array(bool, String?)
A parsed schema value’s well-formedness.
-
.verify(value) ⇒ Array(bool, String?)
Verify that
value(the Rego Value for the schema argument) is a valid JSON schema.
Class Method Details
.match(document_value, schema_value) ⇒ Array(bool, Array), Symbol
:reek:NilCheck :reek:TooManyStatements
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/ruby/rego/builtins/codecs/json_schema_match.rb', line 34 def self.match(document_value, schema_value) schema, schema_error = load_schema(schema_value) return :undefined unless schema_error.nil? return :undefined unless valid_schema(schema).first document, document_error = load_document(document_value) return :undefined unless document_error.nil? # A document or schema nested past Matcher::MAX_DEPTH throws past the recursion; treat as # undefined (the validator declined) rather than risk a SystemStackError. matched = catch(Matcher::TOO_DEEP) { Matcher.new(schema).matches?(document, schema) } return :undefined if matched == Matcher::TOO_DEEP [matched, matched ? [] : [MATCH_ERROR]] end |
.re2_compile(pattern) ⇒ Object
The capped RE2 compilation of pattern, or nil if it is unusable (non-string, invalid encoding,
or over the byte cap). Both caps (compile-phase max_mem, parse-phase bytesize) apply here, so every
compile path shares the same safety: re2_compatible? (verify), re2_valid? (format:regex), and
the Matcher’s per-call cached re2_matches? (the pattern/patternProperties match, which
compiles each distinct pattern at most once). The compiled object is reusable across matches.
517 518 519 520 521 522 |
# File 'lib/ruby/rego/builtins/codecs/json_schema.rb', line 517 def self.re2_compile(pattern) return nil unless pattern.is_a?(String) && pattern.valid_encoding? return nil if pattern.bytesize > RE2_MAX_PATTERN_BYTES RE2::Regexp.new(pattern, log_errors: false, max_mem: RE2_MAX_MEM) end |
.re2_valid?(value) ⇒ Boolean
Whether value is a regex Go’s regexp (RE2) would compile — the format: "regex" assertion,
which gojsonschema implements with the same regexp.Compile as the pattern keyword. Public so
the format checker can reuse the exact compile gate. Callers pass a scannable string.
508 509 510 |
# File 'lib/ruby/rego/builtins/codecs/json_schema.rb', line 508 def self.re2_valid?(value) re2_compatible?(value) end |
.resolve_ref_target(ref, root) ⇒ Object
The schema node a $ref resolves to (for match_schema’s document validation), or nil. The schema
is already known well-formed, so every ref is a resolvable # / #/pointer in-document fragment.
:reek:NilCheck
527 528 529 530 531 532 533 |
# File 'lib/ruby/rego/builtins/codecs/json_schema.rb', line 527 def self.resolve_ref_target(ref, root) return root if ref == "#" || ref.empty? return nil unless ref.start_with?("#/") node = resolve_pointer(ref[1..].to_s, root) node.equal?(NOT_FOUND) ? nil : node end |
.valid_schema(schema) ⇒ Array(bool, String?)
A parsed schema value’s well-formedness. A boolean is a valid schema (matches all / nothing); an object is validated keyword by keyword; anything else is invalid. :reek:NilCheck :reek:TooManyStatements
177 178 179 180 181 182 183 184 185 |
# File 'lib/ruby/rego/builtins/codecs/json_schema.rb', line 177 def self.valid_schema(schema) return [true, nil] if [true, false].include?(schema) return [false, "jsonschema: schema is invalid"] unless schema.is_a?(Hash) error = catch(SCHEMA_TOO_DEEP) { schema_error(schema, schema, RefGuard.new) } return [false, "jsonschema: schema is nested too deeply"] if error == SCHEMA_TOO_DEEP error.nil? ? [true, nil] : [false, "jsonschema: #{error}"] end |
.verify(value) ⇒ Array(bool, String?)
Verify that value (the Rego Value for the schema argument) is a valid JSON schema.
:reek:NilCheck
109 110 111 112 113 114 |
# File 'lib/ruby/rego/builtins/codecs/json_schema.rb', line 109 def self.verify(value) schema, error = load_schema(value) return [false, error] unless error.nil? valid_schema(schema) end |