Skip to content

feat(schema): configurable ingest limits + functional options for NewService#254

Merged
zeevdr merged 1 commit into
mainfrom
feat/schema-limits
Apr 29, 2026
Merged

feat(schema): configurable ingest limits + functional options for NewService#254
zeevdr merged 1 commit into
mainfrom
feat/schema-limits

Conversation

@zeevdr
Copy link
Copy Markdown
Member

@zeevdr zeevdr commented Apr 29, 2026

Summary

Test plan

  • make pre-commit (build, vet, format, lint, test, coverage)
  • New unit tests in internal/schema/limits_test.go cover: DefaultLimits, CreateSchema over MaxFields, CreateSchema exactly at limit, ImportSchema over MaxDocBytes, ImportSchema over MaxFields after parsing
  • Existing service_test.go / service_extended_test.go migrated to the new constructor signature and still pass
  • cmd/server callsite updated; internal/server/memory_integration_test.go migrated and passes
  • Env vars SCHEMA_MAX_FIELDS and SCHEMA_MAX_DOC_BYTES documented in docs/server/configuration.md
  • .agents/context/security-review.md updated with progress note on schema: bound field count, doc size, JSON-Schema compile time #217

Refs #217. Merge before #255.

🤖 Generated with Claude Code

…Service

Lands the first half of #217: bound schema document size and field count at
CreateSchema / ImportSchema, so a single malicious import cannot exhaust the
server before validator caching helps. Compile-timeout for the JSON-Schema
compiler is deferred to a follow-up that touches internal/validation.

- internal/schema/limits.go — new Limits struct (MaxFields, MaxDocBytes) +
  DefaultLimits (10 000 fields, 5 MiB).
- schema.NewService(store, opts...) — store stays positional; logger,
  metrics, validators, and limits move to With...() options. Mirrors the
  config / audit / auth / server functional-options sweep from #235, #236, #249.
- ImportSchema rejects YAML payloads above MaxDocBytes before parsing, and
  CreateSchema / ImportSchema reject schemas above MaxFields after parsing.
  Zero on either dimension means unlimited.
- cmd/server reads SCHEMA_MAX_FIELDS and SCHEMA_MAX_DOC_BYTES env vars via
  the existing parseEnvInt helper and feeds them through WithLimits. Both
  are documented in docs/server/configuration.md.

Refs #217.
@zeevdr zeevdr added this to the Security Review milestone Apr 29, 2026
@zeevdr zeevdr added enhancement New feature or request server Server changes size: M Moderate — a day or two, clear scope priority: P0 Blocks alpha or release labels Apr 29, 2026
@zeevdr zeevdr merged commit 94e6d2b into main Apr 29, 2026
19 checks passed
@zeevdr zeevdr deleted the feat/schema-limits branch April 29, 2026 11:23
zeevdr added a commit that referenced this pull request Apr 29, 2026
Lands the second half of #217 (security review finding 6): bound the
JSON-Schema compiler so a malicious per-field constraint cannot hang
the server during the first compile. Sits on top of #254, which already
caps schema doc bytes and field count at the ingest layer.

- internal/validation/limits.go — new Limits struct (CompileTimeout,
  MaxDepth) + DefaultLimits (5 s, depth 64) + shared Option/WithLimits
  pattern usable by both the factory and individual field validators.
- internal/validation/json_schema.go — newJSONSchemaValidator now takes
  Limits. A pre-compile depth scan rejects pathological nesting before
  invoking the compiler; the Compile call itself runs in a goroutine
  and is bounded by CompileTimeout. jsonschema/v6 has no CompileContext,
  so the timeout is a wall-clock backstop — the goroutine may continue
  past the deadline, but the depth scan + upstream MaxDocBytes cap
  bound the worst-case work.
- NewValidatorFactory(store, opts...) and NewFieldValidator(..., opts...)
  accept the shared Option type; existing zero-opt call sites continue
  to compile unchanged.
- cmd/server reads SCHEMA_COMPILE_TIMEOUT (Go duration) and
  SCHEMA_MAX_REF_DEPTH env vars and threads them through WithLimits on
  the shared validator factory.

Closes #217.
zeevdr added a commit that referenced this pull request Apr 29, 2026
Lands the second half of #217: bound the JSON-Schema compiler so a
malicious per-field json_schema constraint cannot hang the server on
first compile. Sits on top of #254, which already caps schema doc bytes
and field count at the ingest layer.

- internal/validation/limits.go — new Limits struct (CompileTimeout,
  MaxDepth) + DefaultLimits (5 s, depth 64) + shared Option/WithLimits
  pattern usable by both the factory and individual field validators.
- internal/validation/json_schema.go — newJSONSchemaValidator now takes
  Limits. A pre-compile depth scan rejects pathological nesting before
  invoking the compiler; the Compile call itself runs in a goroutine
  and is bounded by CompileTimeout. jsonschema/v6 has no CompileContext,
  so the timeout is a wall-clock backstop — the goroutine may continue
  past the deadline, but the depth scan + upstream MaxDocBytes cap
  bound the worst-case work.
- NewValidatorFactory(store, opts...) and NewFieldValidator(..., opts...)
  accept the shared Option type; existing zero-opt call sites continue
  to compile unchanged.
- cmd/server reads SCHEMA_COMPILE_TIMEOUT (Go duration) and
  SCHEMA_MAX_REF_DEPTH env vars and threads them through WithLimits on
  the shared validator factory.

Closes #217.
zeevdr added a commit that referenced this pull request May 1, 2026
)

PRs #235, #236, #249, #254 replaced struct-config constructors with
functional options across server, gateway, audit, auth, config,
adminclient, and schema. The Config / ServiceConfig / RecorderConfig /
GatewayConfig structs are removed without backward-compat shims (alpha).

This document records before/after for each affected constructor and the
list of With...() options, so SDK consumers and embedders know what to
update when bumping versions.

Closes #275

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 1, 2026
PR #254 added configurable schema ingest limits (MaxFields, MaxDocBytes)
and PR #256 added JSON-Schema compile-time bounds. The validators have
unit tests; this lands the live e2e coverage of the user-facing
rejection paths.

Lower the docker-compose service's caps to small values
(SCHEMA_MAX_FIELDS=100, SCHEMA_MAX_DOC_BYTES=4096) so the new tests can
trip them with cheap payloads. Existing fixtures use at most a few
fields and well under 1 KiB of YAML, so other e2e tests are unaffected.

Add three tests in e2e/validation_limits_test.go:

- TestValidationLimits_MaxFieldsRejected — CreateSchema with 101 fields
  returns InvalidArgument and the message cites the configured cap.
- TestValidationLimits_MaxFieldsAtLimitAccepted — exactly 100 fields is
  accepted; the cap is inclusive at the limit.
- TestValidationLimits_MaxDocBytesRejected — ImportSchema with a YAML
  body padded past the cap is rejected before the parser runs.

The JSON-Schema MaxDepth path is intentionally not covered here:
internal/validation/validator.go silently swallows newJSONSchemaValidator
errors at line 147 (constraints.JsonSchema branch), so a depth-exceeded
schema is accepted at CreateSchema and only surfaces later as a missing
JSON-Schema check. That swallow is worth a separate issue; the depth
trigger itself is unit-tested in internal/validation.

Closes #283

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 1, 2026
The functional-options refactors (PR #235 server, #236 audit/auth, #249
config/adminclient, #254 schema) made every constructor in cmd/server/main.go
take a list of With...() options derived from env vars + cfg flags. The
wiring had no tests, so the bug class "flag is read but never wired into
an option" was silent.

Extract the server + gateway option-construction logic into pure helpers
(buildServerOptions, buildGatewayOptions) that return both the option
slice and a small struct describing the boolean decisions (UseTLS,
UseInsecure, HasRateLimiter). Tests assert on the decisions, catching
the silent-default bug class without inspecting opaque option closures.

Cases covered: TLS vs Insecure (server + gateway); rate limiter wired
when non-nil and absent when nil; extra grpc.ServerOptions are folded
into a single WithGRPCServerOptions and don't inflate the option slice.

Closes #284

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 1, 2026
) (#323)

PRs #235, #236, #249, #254 replaced struct-config constructors with
functional options across server, gateway, audit, auth, config,
adminclient, and schema. The Config / ServiceConfig / RecorderConfig /
GatewayConfig structs are removed without backward-compat shims (alpha).

This document records before/after for each affected constructor and the
list of With...() options, so SDK consumers and embedders know what to
update when bumping versions.

Closes #275

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 1, 2026
…325)

The functional-options refactors (PR #235 server, #236 audit/auth, #249
config/adminclient, #254 schema) made every constructor in cmd/server/main.go
take a list of With...() options derived from env vars + cfg flags. The
wiring had no tests, so the bug class "flag is read but never wired into
an option" was silent.

Extract the server + gateway option-construction logic into pure helpers
(buildServerOptions, buildGatewayOptions) that return both the option
slice and a small struct describing the boolean decisions (UseTLS,
UseInsecure, HasRateLimiter). Tests assert on the decisions, catching
the silent-default bug class without inspecting opaque option closures.

Cases covered: TLS vs Insecure (server + gateway); rate limiter wired
when non-nil and absent when nil; extra grpc.ServerOptions are folded
into a single WithGRPCServerOptions and don't inflate the option slice.

Closes #284

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 3, 2026
* test(e2e): cover validation-limit rejections (#283)

PR #254 added configurable schema ingest limits (MaxFields, MaxDocBytes)
and PR #256 added JSON-Schema compile-time bounds. The validators have
unit tests; this lands the live e2e coverage of the user-facing
rejection paths.

Lower the docker-compose service's caps to small values
(SCHEMA_MAX_FIELDS=100, SCHEMA_MAX_DOC_BYTES=4096) so the new tests can
trip them with cheap payloads. Existing fixtures use at most a few
fields and well under 1 KiB of YAML, so other e2e tests are unaffected.

Add three tests in e2e/validation_limits_test.go:

- TestValidationLimits_MaxFieldsRejected — CreateSchema with 101 fields
  returns InvalidArgument and the message cites the configured cap.
- TestValidationLimits_MaxFieldsAtLimitAccepted — exactly 100 fields is
  accepted; the cap is inclusive at the limit.
- TestValidationLimits_MaxDocBytesRejected — ImportSchema with a YAML
  body padded past the cap is rejected before the parser runs.

The JSON-Schema MaxDepth path is intentionally not covered here:
internal/validation/validator.go silently swallows newJSONSchemaValidator
errors at line 147 (constraints.JsonSchema branch), so a depth-exceeded
schema is accepted at CreateSchema and only surfaces later as a missing
JSON-Schema check. That swallow is worth a separate issue; the depth
trigger itself is unit-tested in internal/validation.

Closes #283

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: P0 Blocks alpha or release server Server changes size: M Moderate — a day or two, clear scope

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant