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 regexp with ^..$, 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 type keyword 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. $defs is deliberately absent: xeipuuv targets draft-04/06/07, so it never validates the draft-2019 $defs (an unknown keyword), and a malformed $defs subschema 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 \C escape (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’s regexp — 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/patternProperties and the format: "regex" assertion). The re2 gem’s C++ RE2 is far slower than Go’s regexp (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/patternProperties check (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.

64 * 1024
RE2_MAX_PATTERN_BYTES =
4 * 1024
MAX_SCHEMA_DEPTH =

Bound on schema-recursion depth (nesting and $ref-chain length). A flat definitions of 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

Class Method Details

.match(document_value, schema_value) ⇒ Array(bool, Array), Symbol

:reek:NilCheck :reek:TooManyStatements

Parameters:

Returns:

  • (Array(bool, Array), Symbol)

    [match, errors], or :undefined when the schema or document argument is not usable (wrong type, malformed JSON string, or a schema that is not well-formed — gojsonschema returns undefined in all of these).



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.

Returns:

  • (Boolean)


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

Returns:

  • (Array(bool, String?))


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

Parameters:

Returns:

  • (Array(bool, String?))

    [valid, error] — error is nil when valid.



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