Validate admin API provisioner payloads; minor status-code fixes - #2755
Validate admin API provisioner payloads; minor status-code fixes#2755coffee4tw wants to merge 9 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LBsTWD57oxThZz44wVy3EL
Authority.UpdateProvisioner wrapped Init() failures as an internal server error (500), unlike StoreProvisioner which correctly treats them as a client-side configuration problem (400). Bad or incomplete provisioner details submitted via UpdateProvisioner now surface as a 400, matching StoreProvisioner's behavior.
PUT (update) is not a resource-creation operation, so it should not return 201 Created. Verified safe against the shipped ca.AdminClient, which only checks for status >= 400.
xhon-pelushi
left a comment
There was a problem hiding this comment.
Read this against master. The direction looks right to me, and the status-code half is a clear improvement β UpdateProvisioner was returning 500 for Init failures while StoreProvisioner already returned ErrorBadRequestType for the same call, so this removes a real create/update asymmetry.
The type/details invariant is a bigger change than the description suggests. On master, ProvisionerToCertificates dispatches purely on the details oneof and never consults p.Type, so on writes the declared type was effectively decorative: a payload with type: ACME and JWK details produced a JWK provisioner recorded as ACME. Enforcing the invariant on Store/Update is the substantive part of this PR, and I think the CHANGELOG entry should say so directly β operators want to know that a payload shape which used to be accepted is now a 400.
One compatibility gap. The description says legacy persisted records stay loadable, and that is true β ProvisionerToCertificates still dispatches on details. But UpdateProvisioner now calls validateProvisionerTypeAndDetails(nu) before anything else, so a legacy record with a mismatched type/details pair can be loaded and used, yet any update to it now fails with a 400 β including edits that have nothing to do with the mismatch, like renaming it or changing its claims. As far as I can see that leaves an operator with no API path to correct the record either, since the corrected payload is the one being rejected. Either allowing an update whose details match the new type (i.e. validating the outcome rather than refusing the request), or documenting the manual fix, would close that. A short note in the CHANGELOG about how to detect affected records would help too.
Error text conflates three cases. provisionerTypeFromDetails returns errors.New("provisioner details are required") when details == nil, when the oneof is unset, and when the oneof is set but the inner message is nil β the third falls out of the switch because each arm only returns inside if d.X != nil. On an admin API that surfaces these as 400 bodies, "details are required" is actively misleading for the last two. Something like "provisioner details for type %s are empty" for the nil-inner case would save someone a debugging session.
A note on the sentinel. linkedca.Provisioner_NOOP is the enum zero value and a real member, so provisionerTypeFromDetails returning it on error means a caller that ever forgets to check err gets a plausible-looking type rather than something obviously invalid. It is not used anywhere else in this repo today, so this is only a robustness point, but returning (Provisioner_Type, bool, error) or naming a dedicated invalid constant would make misuse impossible.
Worth a test: the nil-inner-oneof case is the one that previously reached the type switch β for example ProvisionerDetails_JWK with JWK == nil would have hit json.Unmarshal(d.JWK.PublicKey, ...) and nil-dereferenced. The new guard closes that, which is a nice side effect worth pinning down. I could not convince myself whether protojson can actually construct that shape from an admin API request body (a "jwk": null may just leave the oneof unset), so it may be unreachable from outside β but a unit test asserting the 400 rather than a panic would keep it that way regardless of how the decoding path changes later.
The new tests in provisioners_test.go cover the matching and mismatching cases well; the above is really about the two edges around them.
What
Hardens admin provisioner create/update handling and fixes the provisioner
webhook update response:
POST/PUT /admin/provisioners:new writes reject missing or nil details and require the linkedca details
oneof to match the declared provisioner type.
validation remains in
provisioner.Interface.Init.Initfailures return structured 400 errors instead of 500s.do not match can still load during admin-resource reload; only new
Store/Update writes enforce the invariant.
200 OKinstead of201 Created(createstill returns 201).
[Unreleased].Compatibility
Successful, valid provisioner requests are unchanged. Malformed new writes
now fail with 400s. Read/reload conversion deliberately preserves historical
behavior for legacy mismatched records so an upgrade cannot turn one such
record into a CA startup failure.
The shipped
ca.AdminClienttreats any status below 400 as success on webhookupdate, so the 201β200 correction is compatible with existing clients.
Test plan
missing details, missing oneofs, and typed-nil inner messages.
returning 400.
the admin-resource conversion path.
Initconfiguration returning 400.go test ./...passes on the final pushed checkout.go vet ./authority/...passes.π€ Generated with Claude Code
https://claude.ai/code/session_01LBsTWD57oxThZz44wVy3EL