feat: enforce the schema packs on resourceAttributes, and rename the capability plugins - #17
Conversation
manjudr
left a comment
There was a problem hiding this comment.
Review
Read the whole diff, then verified each substantive claim against the head tree and the published packs in network-specs.
The rename half is clean. No surviving weather/mandi plugin-id references anywhere in the head tree; install/build-plugins.sh, config/oan-provider-adapter.yaml (providerSteps + steps), package clauses and imports all move together; step resolution in core/module/handler/stdHandler.go:757 is an exact case-sensitive match; .github/workflows/build-and-deploy-plugins.yml derives the .so name from the directory basename, so it follows automatically. go vet and go test ./pkg/plugin/implementation/schemav2validator/... pass.
The per-key strip logic is sound. stripUnaccountedJSONLDKeys / schemaDeclaresProperty are cycle-guarded, non-mutating, and correctly exclude not and if. Asking the schema per key rather than adding a config switch is the right call, and the comment explaining why earns its length.
Every problem is in switching the layer on. Five findings inline — two on the config, one on the new test, two file-level on extended_schema.go (pre-existing code, but this PR is what makes it reachable, so it's in scope).
Suggested sequencing
The two config findings together mean the adapter rejects its own traffic the moment it deploys, and no test in the repo can catch it. Either:
- fix the captured payloads + packs and add
schemas.openagrinet.globalto the allowlist before merging, or - land the validator code with
extendedSchema_enabled: "false"and flip it in a follow-up, once a test exercises a real capture through the fullschemav2validatorpath.
The $ref allowlist bypass should be fixed either way, independent of the flag.
| extendedSchema_enabled: "false" | ||
| extendedSchema_cacheTTL: "86400" | ||
|
|
||
| extendedSchema_enabled: "true" |
There was a problem hiding this comment.
Enabling this rejects the repo's own verbatim captured /select payloads.
WeatherObservation/mappings_test.go:45 is labelled "the verbatim /select captured from the OAN network". Its resourceAttributes:
{
"@context": "https://schemas.openagrinet.global/schema/WeatherObservation/v0.1/context.jsonld",
"@type": "openagrinet:WeatherObservation",
"subjectCategories": ["Weather"],
"location": { "type": "Point", "coordinates": [73.7898, 19.9975] },
"validity": { "startsAt": "2026-08-26", "endsAt": "2026-08-30" }
}Two independent failures against AgricultureResource/v0.1/attributes.yaml:
informationModeis missing.attributes.yaml:17putsrequired: [informationMode]on the root object — not under athen, so the if/then/else gap this block honestly documents at lines 141-145 does not excuse it.validity.startsAt/endsAtare bare dates.attributes.yaml:138and:143declareformat: date-time, and the validator is configured withEnableFormatValidation()."2026-08-26"is not a date-time.
The mandi capture fails the same way.
What makes this worse than an ordinary bug: mappings_test.go calls the mapping step directly and never traverses schemav2validator, so CI stays green while the deployed adapter refuses the exact payload those tests assert it serves. Worth adding a test that pushes one capture through the full validator so this can't regress silently.
There was a problem hiding this comment.
Both failures are real. I verified each against the pack rather than the description, because "not under a then" is the whole load-bearing part of your first point:
informationMode—required: [informationMode]sits at the root ofAgricultureResource, confirmed by walking the schema and printing everyrequiredwith its path. Root, not underif/then. So the if/then gap does not excuse it, exactly as you say.validity.startsAt/endsAt—format: date-timeat pack lines 138 and 143, andEnableFormatValidation()is on atextended_schema.go:625."2026-08-26"fails it.
Your point about why this is worse than an ordinary bug is the one I would most want kept: mappings_test.go calls the mapping step directly and never traverses schemav2validator, so CI stays green while the deployed adapter refuses the exact payload those tests assert it serves. That is a test suite that cannot see the failure it is meant to catch.
Not fixing it in this PR, and leaving this thread open. Payload and mapping changes are being batched separately from code fixes, and the end-to-end test you suggest belongs with them — a test pushing a capture through the full validator would fail today, so it lands with the payload fix or not at all.
One thing to settle before that batch, since it changes what the fix should be. Adding informationMode satisfies the required field, but the only permitted values are OnDemand and Direct, and OnDemand's then forbids location, market and validity — the exact fields a select must carry. So informationMode: "OnDemand" passes only because if/then is never evaluated, trading a visible non-conformance for an invisible one that a conformant validator would reject. The packs have no query mode; that gap is raised on PR #13 and is a spec change rather than ours.
Neither the pack nor the mappings are on a pinned version yet — the pack is a branch ref and MAPPING_URL points at a branch — so both are expected to move, and this gets revisited when they settle.
| # FAILS rejects the payload -- it does not skip validation, which | ||
| # is the right way round, but it does mean this adapter needs | ||
| # egress to the host allowed below. | ||
| extendedSchema_allowedDomains: "raw.githubusercontent.com" |
There was a problem hiding this comment.
This allowlist excludes the canonical @context host, so only non-canonical contexts can pass.
schemas.openagrinet.global is what the packs publish in their own x-jsonld and context.jsonld, and it is what every fixture on this branch uses — see WeatherObservation/mappings_test.go:62. With only raw.githubusercontent.com allowed, extended_schema.go:598 returns SCH_INVALID_JSONLD_CONTEXT before any fetch is attempted.
The net effect is inverted from the intent: a payload pointing at the canonical schema host is rejected, while a payload pointing at an arbitrary GitHub user's raw content is accepted. Add schemas.openagrinet.global, and keep raw.githubusercontent.com only if something actually needs it (see the file-level note on extended_schema.go).
There was a problem hiding this comment.
The diagnosis is right, and I checked every step of it: the fixtures on this branch do use schemas.openagrinet.global, the allowlist does permit only raw.githubusercontent.com, and extended_schema.go:598 does refuse before any fetch. Your "the net effect is inverted from the intent" is a fair reading of it.
Leaving the allowlist as it is for now, and leaving this thread open rather than closing it as handled. Config changes on this branch are being batched separately from code fixes, so this is deferred, not dismissed.
One thing that does change the remedy, though. I checked whether the canonical host serves anything, and schemas.openagrinet.global has no DNS at all, while raw.githubusercontent.com resolves.
So allowlisting the canonical host does not make those payloads validate — it converts an immediate, clearly-coded SCH_INVALID_JSONLD_CONTEXT into a fetch that waits out the download timeout and then fails as SCH_SCHEMA_ADAPTATION_FAILED. Same rejection, later and less legibly. In JSON-LD the @context is an identifier rather than a fetch target, which is why the packs can name a host that does not serve — but this validator resolves a schema from it, so it needs one that answers.
That makes it a choice between two things, and it is worth being explicit about which:
- allowlist the canonical host and fix the fixtures to point at a host that resolves — the identifier stays canonical, the fetch target has to be real;
- or keep the raw mirror as the
@contextand record in the config why the canonical host is deliberately absent.
Your point about arbitrary GitHub raw content stands under either, and it connects to the file-level $ref comment: raw.githubusercontent.com is world-writable, so allowlisting it trusts every GitHub account. Narrowing it to a path prefix or mirroring the packs somewhere we control would be the real fix, and neither is a one-line allowlist edit.
Also relevant to whatever gets chosen: the allowlist cannot shrink to one host once $refs are host-checked. Loading one pack pulls 15 documents across three hosts — the one named above plus two external spec hosts the packs $ref into — so all three have to be in it. Measured, not assumed; details in the reply on the $ref thread.
manjudr
left a comment
There was a problem hiding this comment.
Requesting changes
Formalising the previous review as a blocking one. The detail is in the inline comments there; this is the short version of what needs to change before merge.
Blocking:
-
config/oan-provider-adapter.yaml:154— withextendedSchema_enabled: "true", the adapter rejects this repo's own verbatim captured/selectpayloads:informationModeis missing (required atAgricultureResource/v0.1/attributes.yaml:17, on the root object, not under athen), andvalidity.startsAt/endsAtare bare dates againstformat: date-time. Both the weather and mandi captures fail.mappings_test.gonever traversesschemav2validator, so CI stays green while the deployed adapter refuses the exact payload those tests assert it serves. -
config/oan-provider-adapter.yaml:174—extendedSchema_allowedDomainsomitsschemas.openagrinet.global, the canonical@contexthost the packs publish and every fixture here uses. Only non-canonical contexts can pass. -
extended_schema.go:598/:345— the allowlist gates only the top-level@context;$refs in the fetched schema reachfreshReadFromURIwith no host or scheme check. Sinceraw.githubusercontent.comis writable by any GitHub account, an unauthenticatedselectcan drive arbitrary outbound fetches or local file reads. Pre-existing code, but this PR is what makes it reachable — should be fixed regardless of the flag's state.
Non-blocking but worth addressing: the array-form @type silent skip and the unreachable wrong-@type test, both in the inline comments.
Either fix (1) and (2) before merge, or land the validator code with extendedSchema_enabled: "false" and flip it in a follow-up once a test pushes a real capture through the full validator path. The rename half and the per-key JSON-LD strip logic are good to go as-is.
033807d to
284ce62
Compare
f2130e6 to
59f2d3c
Compare
Review round: what is fixed here, and what is deliberately notAll five comments were genuine — each verified against the code and the packs before changing anything, and each fix has a test that fails without it. Fixed in this branch
Worth recording one measurement from the first of those, since it decided the scope: loading a single capability pack pulls 15 documents across 3 hosts, and the allowlist was checked on exactly one of them. The local-file read is now closed unconditionally, with no config change. Deliberately not fixed here — threads left open Two findings need a config or payload change, which this branch is batching separately from code fixes. Their threads are left unresolved rather than closed, so they are not mistaken for handled:
One thing not done that a reader might expect. Enforcing the allowlist on Neither the schema packs nor the mappings are on a pinned version yet, so both are expected to move. |
fa2d4d5 to
59421ba
Compare
983affb to
ed76651
Compare
59421ba to
d6033a8
Compare
🛡️ Trivy security scan (CRITICAL,HIGH,MEDIUM,LOW)Go dependenciesNo findings at CRITICAL,HIGH,MEDIUM,LOW. Container imageNo findings at CRITICAL,HIGH,MEDIUM,LOW. |
|
📊 Test Coverage: ✅ Passed — 92% of changed lines covered, min 80% |
d6033a8 to
6f3ab95
Compare
c59783c to
2767495
Compare
6f3ab95 to
c481e7a
Compare
c481e7a to
5fcfb2f
Compare
Extended validation removed @context and @type from every object before validating it. That suited a schema which closes itself with additionalProperties:false and never mentions either key, and it made a schema pack that DECLARES @type and lists it in required impossible to satisfy: the payload carried @type, the schema required it, and the validator had just taken it out, so a conforming payload was rejected for a missing field it had supplied. Decide per key, by asking the schema whether it declares that key -- as a property or in required, anywhere in its composition tree, since the OAN packs declare @type one level down in allOf. Both schema styles then validate without a config switch and without either having to know about the other. Keeping @type also means its const is now checked, so a resource whose @type is not the one the capability declares no longer passes silently. [#16] Please enter the commit message for your changes. Lines starting [#16] with '#' will be ignored, and an empty message aborts the commit. [#16] [#16] Date: Mon Sep 7 13:22:43 2026 +0530 [#16] [#16] interactive rebase in progress; onto 983affb [#16] Last command done (1 command done): [#16] reword e19177c fix(schemav2validator): keep the JSON-LD keys a schema declares [#16] [#16] Next commands to do (5 remaining commands): [#16] reword 247022c refactor(plugins): name the capability plugins after their capabilities [#16] [#16] reword ec87958 feat(config): validate resource attributes against their schema packs [#16] [#16] You are currently editing a commit while rebasing branch 'tmp/rebase-16' on '983affb'. [#16] [#16] Changes to be committed: [#16] modified: pkg/plugin/implementation/schemav2validator/extended_schema.go [#16] modified: pkg/plugin/implementation/schemav2validator/extended_schema_test.go [#16]
…es [#16] weather and mandi named a domain; the payloads they serve name a capability. Renaming the packages to WeatherObservation and MandiPrice puts the two in the same vocabulary, so a binding key, a schema pack and the plugin that answers for it all read the same. The .so basename is the plugin id the adapter config refers to, so the directory rename carries the ids with it -- providerSteps and steps in config/oan-provider-adapter.yaml move together with the packages, and build-plugins.sh with them. No behaviour changes: the whole suite passes, and both plugins still build as loadable shared objects. Both package docs claimed one package per schema pack FAMILY, which the new names contradict. They now say what is true -- one package per capability, named for the capability, with the binding keys it answers to still configuration. [#16] Please enter the commit message for your changes. Lines starting [#16] with '#' will be ignored, and an empty message aborts the commit. [#16] [#16] Date: Mon Sep 7 13:23:03 2026 +0530 [#16] [#16] interactive rebase in progress; onto 983affb [#16] Last commands done (2 commands done): [#16] reword e19177c fix(schemav2validator): keep the JSON-LD keys a schema declares [#16] [#16] reword 247022c refactor(plugins): name the capability plugins after their capabilities [#16] [#16] Next commands to do (4 remaining commands): [#16] reword ec87958 feat(config): validate resource attributes against their schema packs [#16] [#16] reword bc8979b refactor(config): resolve capability schemas from the payload's @context [#16] [#16] You are currently editing a commit while rebasing branch 'tmp/rebase-16' on '983affb'. [#16] [#16] Changes to be committed: [#16] modified: config/oan-provider-adapter.yaml [#16] modified: install/build-plugins.sh [#16] renamed: pkg/plugin/implementation/mandi/mandi.go -> pkg/plugin/implementation/MandiPrice/MandiPrice.go [#16] renamed: pkg/plugin/implementation/mandi/cmd/plugin.go -> pkg/plugin/implementation/MandiPrice/cmd/plugin.go [#16] renamed: pkg/plugin/implementation/mandi/mappings_test.go -> pkg/plugin/implementation/MandiPrice/mappings_test.go [#16] renamed: pkg/plugin/implementation/mandi/prerequisites.go -> pkg/plugin/implementation/MandiPrice/prerequisites.go [#16] renamed: pkg/plugin/implementation/weather/weather.go -> pkg/plugin/implementation/WeatherObservation/WeatherObservation.go [#16] renamed: pkg/plugin/implementation/weather/cmd/plugin.go -> pkg/plugin/implementation/WeatherObservation/cmd/plugin.go [#16] renamed: pkg/plugin/implementation/weather/cmd/plugin_test.go -> pkg/plugin/implementation/WeatherObservation/cmd/plugin_test.go [#16] renamed: pkg/plugin/implementation/weather/mappings_test.go -> pkg/plugin/implementation/WeatherObservation/mappings_test.go [#16] renamed: pkg/plugin/implementation/weather/prerequisites.go -> pkg/plugin/implementation/WeatherObservation/prerequisites.go [#16]
…#16] The base validator treats resourceAttributes as a free-form object -- the envelope is correct whatever a capability puts inside it. Extended validation checks the inside: it walks the payload for objects carrying @context and @type, resolves the schema @type names, and validates against it. Until the JSON-LD strip was fixed no pack could pass, so the keys were configured and the layer was off. Resolution is local, not a fetch per payload. Every schema under extendedSchema_localSchemaPath is loaded at startup and looked up by @type, so a select costs no network call and works with no egress. The schemas are published elsewhere and deliberately not copied in here; a copy would drift, and pinning a revision in adapter config would make a deployment decision on the deployment's behalf. The comment states the layout to mount, $ref targets included. A missing directory fails startup rather than degrading, which is the behaviour to want -- the alternative is accepting unvalidated payloads because a mount was forgotten. The allowlist is narrowed from raw.githubusercontent.com to the host the packs' own @context names, so a local miss fails loudly instead of quietly fetching from elsewhere. Recorded in the comment because a green result is otherwise misleading: the validator library parses if/then/else but never evaluates it, so a pack's conditional rules are not enforced. In the OAN packs that is everything predicated on informationMode. [#16] Please enter the commit message for your changes. Lines starting [#16] with '#' will be ignored, and an empty message aborts the commit. [#16] [#16] Date: Mon Sep 7 13:23:24 2026 +0530 [#16] [#16] interactive rebase in progress; onto 983affb [#16] Last commands done (3 commands done): [#16] reword 247022c refactor(plugins): name the capability plugins after their capabilities [#16] [#16] reword ec87958 feat(config): validate resource attributes against their schema packs [#16] [#16] Next commands to do (3 remaining commands): [#16] reword bc8979b refactor(config): resolve capability schemas from the payload's @context [#16] [#16] reword 6385051 refactor: name the registry and binding packages for what they are [#16] [#16] You are currently editing a commit while rebasing branch 'tmp/rebase-16' on '983affb'. [#16] [#16] Changes to be committed: [#16] modified: config/oan-provider-adapter.yaml [#16]
[#16] Extended validation was pointed at a mounted directory of schema files, because the @context the payloads declared -- schemas.openagrinet.global -- does not resolve, and a failed fetch rejects the payload. That put the burden on every deployment to place the right files at the right path, and made the adapter refuse to start when one did not. The published packs do serve context.jsonld, so the fetch the validator already knows how to do works once @context names them: @context .../network-specs/schema-packs-v0.1/schema/MandiPrice/v0.1/context.jsonld fetched .../network-specs/schema-packs-v0.1/schema/MandiPrice/v0.1/attributes.yaml So localSchemaPath is gone, and with it the mount, the directory and the script that filled it. The revision a payload is judged against now comes from the payload, which is a better place for it than adapter config: nothing here can go stale against what the network publishes. The allowlist moves to raw.githubusercontent.com, the host that @context now resolves to. It is doing real work rather than naming a host that never answered -- an @context on any other host is refused before a fetch is attempted. Two costs, both stated in the config: this adapter now needs egress to that host, and the first payload after a restart pays for the fetch. Measured at about 2s, cached for 24h after that. Verified in oan-local: publish and select both pass, the first payload logs "fetching from network" and later ones "LRU cache hit", a foreign @context is refused with SCH_INVALID_JSONLD_CONTEXT, and the collection is 51 of 51. [#16] Please enter the commit message for your changes. Lines starting [#16] with '#' will be ignored, and an empty message aborts the commit. [#16] [#16] Date: Mon Sep 7 14:17:15 2026 +0530 [#16] [#16] interactive rebase in progress; onto 983affb [#16] Last commands done (4 commands done): [#16] reword ec87958 feat(config): validate resource attributes against their schema packs [#16] [#16] reword bc8979b refactor(config): resolve capability schemas from the payload's @context [#16] [#16] Next commands to do (2 remaining commands): [#16] reword 6385051 refactor: name the registry and binding packages for what they are [#16] [#16] reword 9ebb5c1 docs(config): placeholder the subscriber id, keep the old value in a comment [#16] [#16] You are currently editing a commit while rebasing branch 'tmp/rebase-16' on '983affb'. [#16] [#16] Changes to be committed: [#16] modified: config/oan-provider-adapter.yaml [#16]
Three renames, and one rule applied across the tree.
pkg/plugin/implementation/oanregistry -> sunbirdRegistry
pkg/plugin/implementation/internal/oanbinding -> internal/capabilitybinding
config/oan-provider-adapter.yaml -> config/provider-adapter.yaml
capabilitybinding rather than keybinding, which was the suggestion: this
package derives a {ParticipantID, CapabilityCode} pair and renders it as the
participant|capability key the registry indexes on. "Capability binding" is
already the term the code, the registry schema and the config all use, so the
package now says it. "Key" would have been actively misleading -- this codebase
uses key for signing keys, which is a different thing resolved by a different
plugin.
The organisation name is gone from every filename, identifier, comment, error
string and trace span name. Two categories were deliberately left alone
because they are DATA rather than naming:
- openagrinet:WeatherObservation and its siblings are capability codes on
the wire. Changing them would change the protocol.
- participant ids and names inside captured registry fixtures
(provider.oan.local, "OAN provider layer adapter") are verbatim responses.
Editing them would make the fixture stop matching what a registry returns,
which is the only reason the fixture is worth having.
Two consequences worth knowing. The .so basename is the plugin id, so
`id: oanregistry` becomes `id: sunbirdRegistry` and any deployment's config
moves with the image. And pluginID is a telemetry attribute, so traces and
metrics from this plugin now report sunbirdRegistry -- it follows the rename
rather than reporting a name that no longer exists.
The two cache key prefixes changed with it, oan_lookup_ and oan_provider_ to
registry_lookup_ and registry_provider_. They are cache namespaces, not metric
names, so the cost is one cold cache cycle.
Full suite green at 63 packages, vet clean, and sunbirdRegistry.so builds.
[#16] Please enter the commit message for your changes. Lines starting
[#16] with '#' will be ignored, and an empty message aborts the commit.
[#16]
[#16] Date: Mon Sep 7 17:15:52 2026 +0530
[#16]
[#16] interactive rebase in progress; onto 983affb
[#16] Last commands done (5 commands done):
[#16] reword bc8979b refactor(config): resolve capability schemas from the payload's @context [#16]
[#16] reword 6385051 refactor: name the registry and binding packages for what they are [#16]
[#16] Next command to do (1 remaining command):
[#16] reword 9ebb5c1 docs(config): placeholder the subscriber id, keep the old value in a comment [#16]
[#16] You are currently editing a commit while rebasing branch 'tmp/rebase-16' on '983affb'.
[#16]
[#16] Changes to be committed:
[#16] modified: config/mappings/agmarknet/mandi-price.select.yaml
[#16] renamed: config/oan-provider-adapter.yaml -> config/provider-adapter.yaml
[#16] modified: install/build-plugins.sh
[#16] modified: pkg/plugin/definition/mapper.go
[#16] modified: pkg/plugin/implementation/WeatherObservation/mappings_test.go
[#16] renamed: pkg/plugin/implementation/internal/oanbinding/oanbinding.go -> pkg/plugin/implementation/internal/capabilitybinding/capabilitybinding.go
[#16] renamed: pkg/plugin/implementation/internal/oanbinding/oanbinding_test.go -> pkg/plugin/implementation/internal/capabilitybinding/capabilitybinding_test.go
[#16] renamed: pkg/plugin/implementation/internal/oanbinding/paths.go -> pkg/plugin/implementation/internal/capabilitybinding/paths.go
[#16] modified: pkg/plugin/implementation/internal/upstream/upstream.go
[#16] modified: pkg/plugin/implementation/internal/upstream/upstream_test.go
[#16] modified: pkg/plugin/implementation/jsonmapper/README.md
[#16] modified: pkg/plugin/implementation/schemav2validator/extended_schema.go
[#16] modified: pkg/plugin/implementation/schemav2validator/extended_schema_test.go
[#16] renamed: pkg/plugin/implementation/oanregistry/README.md -> pkg/plugin/implementation/sunbirdRegistry/README.md
[#16] renamed: pkg/plugin/implementation/oanregistry/cmd/plugin.go -> pkg/plugin/implementation/sunbirdRegistry/cmd/plugin.go
[#16] renamed: pkg/plugin/implementation/oanregistry/cmd/plugin_test.go -> pkg/plugin/implementation/sunbirdRegistry/cmd/plugin_test.go
[#16] renamed: pkg/plugin/implementation/oanregistry/providerrecord.go -> pkg/plugin/implementation/sunbirdRegistry/providerrecord.go
[#16] renamed: pkg/plugin/implementation/oanregistry/providerrecord_test.go -> pkg/plugin/implementation/sunbirdRegistry/providerrecord_test.go
[#16] renamed: pkg/plugin/implementation/oanregistry/oanregistry.go -> pkg/plugin/implementation/sunbirdRegistry/sunbirdRegistry.go
[#16] renamed: pkg/plugin/implementation/oanregistry/oanregistry_test.go -> pkg/plugin/implementation/sunbirdRegistry/sunbirdRegistry_test.go
[#16]
…nts [#16] The plugins are generic and their comments should read that way: jsonmapper knows nothing about any provider, and the validator's if/then gap applies to any pack, not to one network's. This commit used to also placeholder the subscriberId. That work is now in the base branch, so what is left here is the comment wording alone, and the message says so rather than claiming a change that is no longer in the diff.
… schema [#16] The allowlist was consulted once, on the top-level @context. Every document fetched to resolve that document's $refs met no check at all, and freshReadFromURI falls through to os.ReadFile for any scheme but http and https. So a $ref of "file:///etc/passwd" -- or a bare path, which parses with no scheme -- was an instruction from the network to open this container's disk and parse it as a schema. Measured: one capability pack pulls 15 documents, so 14 of the 15 reads were unchecked. A loader on this path now refuses anything but http and https, for the entry document and every $ref under it. The base spec loader keeps the file fallthrough deliberately: its location is operator-configured, where a local file is the point. localSchema mode keeps it for the same reason. This does not restrict which HOSTS a $ref may reach, and that is deliberate rather than missed: the packs $ref two external spec hosts, so enforcing the allowlist on refs needs those named in it as well, or no pack loads at all. TestValidateReferencedObject_AllowsARefToAHostOutsideTheAllowlist pins that, so the wider change cannot be made without noticing. Two existing tests asserted the old behaviour rather than a requirement, and say the new one now; several others used a local temp file as a fixture and either load it in operator mode or serve it over http, which is what production does anyway.
…it cannot read [#16] findReferencedObjects required a string for both JSON-LD keys, so an object carrying the list form matched neither assertion, was never appended, and its resourceAttributes went to no schema at all -- while the extended layer reported a pass over an object it had not looked at. The list form is not exotic. The packs declare @type as a oneOf whose second branch is a list carrying the canonical OAN type alongside provider-defined ones, so a conforming payload could use it and be skipped. Which entry names the capability is not fixed either, so the document now decides: the first @type it declares a schema for wins, rather than the payload's ordering. @context takes the list form too, using the first string in it, because only a URL locates a schema and an inline object names no document to fetch. Skipping is replaced by rejection wherever the object claims a type this validator cannot read. @type ABSENT is left alone and still passes over: an object with a context and no type makes no claim about which schema applies, and there is nothing to validate it against. Also replaces the @type const test. It built obj.Type and Data["@type"] disagreeing, which the real path cannot produce -- both are read off one map -- so it demonstrated nothing about a payload. Tests now run through findReferencedObjects, and the const does have a payload-level case: the list branch forbids a second openagrinet: type, which is caught only because @type is kept in the data rather than stripped.
80dd82d to
75f87f0
Compare
5fcfb2f to
9a1594f
Compare
Code reviewFound 2 issues (head
network-adapter/config/provider-adapter.yaml Lines 191 to 211 in 9a1594f
network-adapter/pkg/plugin/implementation/MandiPrice/prerequisites.go Lines 14 to 19 in 9a1594f Separately, note that manjudr's 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…ad [#16] The allowlist guarded the entry @context only. The document that came back was then trusted to name anything: its $refs resolved through the loader with no host check, so a payload could name an attacker's document on the allowed host and have this process fetch whatever that document pointed at -- an internal service, a cloud metadata endpoint. Server-side request forgery, driven by an unauthenticated field. The reach was the argument for fixing it rather than documenting it: loading one capability pack pulls 13-16 documents, of which exactly one is the entry. The refs were never the corner case, they were the traffic. The same allowlist is now checked on every read. Two refusals with different scopes, because they answer different questions: scheme http/https only, and only on the payload-directed path. An operator who set extendedSchema_localSchemaPath is asking for files to be read, so the refusal must not apply to them -- installing it on both branches broke exactly that, which TestLoadSchemaFromPath_LocalFile caught. host on BOTH paths. localSchema falls back to the network for a ref it does not hold locally, so it had the same exposure by a longer route. THE ALLOWLIST HAD TO GROW, and this is a code+config pair that cannot land half-applied. Measured, not assumed: every pack -- WeatherObservation, MandiPrice, KnowledgeResource -- touches raw.githubusercontent.com, schema.beckn.io and schema.nfh.global. With the previous single host and refs checked, the real packs fail with SCH_SCHEMA_ADAPTATION_FAILED on every payload; with the three named, they load and validate. Both directions are verified against the published packs. The test that pinned the old behaviour is inverted rather than deleted: it now asserts a cross-host $ref is refused, and a second test asserts the chain loads once both hosts are named -- which is the case the packs need. What this does NOT fix: raw.githubusercontent.com is world-writable, so the allowlist still trusts every GitHub account for schema content. Narrowing to a path prefix, or mirroring the packs on a host we control, is the real fix and is not a one-line change. Said so in the config.
The prerequisites note pointed at weather/prerequisites.go, which this branch renamed to WeatherObservation/. It was the last stale plugin path in a Go comment.
Both fixed — and the first one changed my mind about a decision I'd defended1 · SSRF via I had left this deliberately and pinned it with a test, on the grounds that host-enforcing refs breaks pack loading. That reasoning was sound about the mechanism and wrong about the conclusion: I treated "the allowlist can't be one host" as a reason not to check refs, when it was a reason to fix the allowlist. Your framing — that the returned document is not trusted because a payload named it, on a host anyone can publish to — is the part I'd underweighted. The reach is the argument for fixing rather than documenting. Loading one pack pulls 13–16 documents, of which exactly one is the entry. The refs were never the corner case; they were the traffic. The same allowlist is now checked on every read. Two refusals with different scopes, because they answer different questions:
The allowlist had to grow, and this is a code+config pair that cannot land half-applied. Measured against the published packs, not assumed — every pack ( The test that pinned the old behaviour is inverted, not deleted: it now asserts a cross-host What this does not fix, and I'd rather name it than let it read as closed: One deployment coupling this creates: 2 · Stale path after the rename — On the two open blocking conditions — both still stand, unchanged and deliberately:
Both are payload/config work, batched separately from code fixes and tracked by the two unresolved threads above. Verified: |
Targets
feat/8-mandi-pluginrather thandevelopment, because it changes both capability plugins that PR #13 introduces. That makes this the third in a stack — merge #2, then #13, then this one.What
Extended schema validation, on for the first time, plus the rename that makes a plugin id read as the capability it serves. 13 files, +359 / -61, four commits.
The validator —
pkg/plugin/implementation/schemav2validator/extended_schema.goThe one substantive code change, and the only reason the layer could not be switched on before.
validateReferencedObjectstripped both@contextand@typefrom every object before validating it, on the reasoning that they are JSON-LD plumbing the validator handles itself. The OAN packs disagree: they declare@typeand list it inallOf[].required. So a conforming payload was rejected for a missing@typethat it had sent and the validator had just removed, and nothing in configuration could bridge it.The fix is not "stop stripping
@type". That would break the other schema style, which the plugin's own tests use — a schema that closes itself withadditionalProperties: falseand never mentions either key rejects the payload if@typeis left in. The two styles need opposite treatment, so the decision is made per key by asking the resolved schema whether it declares that key, as a property or inrequired, anywhere in its composition tree. The packs declare@typeone level down insideallOf, so the walk coversallOf/anyOf/oneOfand thethen/elsebranches.notandifare deliberately not walked. Naming a property undernotforbids it and underifonly selects a branch, so a match in either says nothing about whether the key is permitted.Keeping
@typehas a second effect worth noting: itsconstis now checked, so a resource claiming a@typeits capability does not declare stops passing silently.Where the schemas come from — no mount, no vendored copy
Each resource's
@contextnames the published pack, and the validator swapscontext.jsonldforattributes.yamlto fetch the schema beside it. So the pack revision a payload is judged against comes from the payload, and nothing in this repo can go stale against what the network publishes.A local mounted directory was built first and then removed. It existed only because the
@contextthe payloads carried did not resolve, and a failed fetch rejects the payload — but the published packs do servecontext.jsonld, so pointing@contextat them makes the fetch the validator already knew how to do work, and the mount, the fetch script and a startup dependency all became unnecessary.extendedSchema_allowedDomainsis narrowed to the host@contextactually resolves to, and is now load-bearing rather than naming a host that never answered: an@contexton any other host is refused before a fetch is attempted.The rename —
weather→WeatherObservation,mandi→MandiPriceMechanical, but breaking for any deployment. A step name that is not one of the built-ins is looked up among the loaded plugins, and a plugin's id is the basename of its
.so, so the directory rename carries the config ids with it:providerStepsandstepsinconfig/oan-provider-adapter.yamland the entry ininstall/build-plugins.shmove together with the packages. An adapter running this config against an image built before the rename exits withunrecognized step: WeatherObservation, which reads like a config typo and is not — config and image have to roll forward and back together.Both package docs claimed "one package per schema pack family", which the new names contradict. They now say what is true: one package per capability, named for the capability, with the binding keys it answers to still configuration.
Why
Base validation checks the envelope and treats
resourceAttributesas a free-form object, so until now nothing checked a capability's own attributes at all. A wrong unit, a missing required attribute or a field the pack does not define all passed, and became something a consumer discovered later. The plugin already had the machinery; it was configured and never ran.Testing
Full suite green — 63 packages. Four new tests cover the per-key strip, including a pack-shaped schema that requires
@type, one that rejects a wrong@type, and the composition walk.End to end against a running stack, on an image built from this branch, with both plugins loading under their new ids: publish and select pass for both capabilities, the first payload logs
fetching from networkand every later oneLRU cache hit, and the local collection is 51 of 51.Negative controls, so a pass reads as real checking rather than an absence of it — each rejected with
SCH_SCHEMA_VALIDATION_FAILEDand the JSON path that failed:enumdate-timefieldschema.beckn.io$refAnd an
@contexton a host outside the allowlist rejected withSCH_INVALID_JSONLD_CONTEXT, before any fetch.Notes for review
if/then/elseis not enforced, and a green result is misleading without knowing that.kin-openapiv0.144.0 has the struct fields, so the keywords look supported, butvisitJSONnever dispatches them. Verified empirically rather than read off the struct. Enforced: types, string formats,enum,const,required,minItems,additionalProperties,not,allOf/anyOf/oneOf.informationModeand allow onlyOnDemandorDirect.OnDemandforbids exactly the fields aselectmust carry to be a question —locationfor weather,marketandvalidityfor mandi — whileDirectrequires answer data a consumer does not have. There is no query mode, so ourselectrequests are not pack-conformant on paper and pass only because the forbidding sits underthen.parameters[].aggregation, which the pack forbids: its parameter item is{parameter, value, unit}withadditionalProperties: false, and theparameterenum has no minimum or maximum variants, so a daily minimum and maximum temperature cannot be distinguished at all. Nothing validates responses today, so it does not bite until a consumer checks. It needs a change to the packs.OpenAgriNet/helmchartsonfeat/4-docker-compose: the renamed ids, the same validator settings, and a pinned adapter image so the shipped default cannot meet a pre-rename build.[#8]rather than[#16]. Issue Adapter: validate resourceAttributes against the schema packs, and name the plugins after their capabilities #16 was opened after the work, and the SHAs are referenced from it and from a published image tag, so rewriting them would cost more than the inconsistency.Closes #16