Task · GCV-0081

GrafanaProvisioningRepository can only express the secure-value shape the vendor refuses, so setting any repository secret 403s forever

Description

GCV-0074 established that the vendor refuses a provisioning secure value passed as a NAME REFERENCE, returning ‘403 PermissionDenied: identity type access-policy not allowed, expected either user or service-account: invalid identity’ - an error about neither the caller nor an access policy. Only the inline create form, base64 encoded, is accepted. That was fixed for GrafanaProvisioningConnection by emitting the create form.

THE SIBLING API STILL ONLY SPEAKS THE REFUSED SHAPE. GrafanaProvisioningRepository exposes spec.repository.secure with token, webhookSecret and commitSigningKey, and every one of them is constrained to the reference form on both sides:

So a consumer who sets any repository secret has no expressible shape that the vendor will accept, and no way to reach the one that works. Repository and Connection are the SAME vendor API group, provisioning.grafana.app/v0alpha1, and the 403 was traced to that group’s secure-value resolution path rather than to the Connection kind, so the same refusal is expected here. That expectation is the thing to settle first: it is inference from a sibling kind, not an observation of this one.

WHY IT LOOKS PROVEN AND IS NOT. GCV-0074 records that the repository API ‘vends correctly and reconciles clean’. That observation was made on a repository with NO secure values - authentication came from the connection - so it says nothing about this path. A green vend of the default path was read as a green vend of the kind.

THE CONSEQUENCE IS NOT CONFINED TO THIS PLATFORM, which is what raises the priority. The refused request is retried by Crossplane roughly every three minutes for as long as the claim exists. Each attempt makes the vendor’s provisioning app fail to read the referenced secure value, and that is what raised an alert on the vendor’s own on-call the first time this class was hit, on a weekend, on a live stack. A consumer who sets repository.secure.token today reproduces that. Nothing in this repository warns them.

WHAT MAKES THE FIX AWKWARD, so it is triage rather than a one-liner. The claim-side prohibition on credential literals is deliberate and the Connection fix kept it, narrowing only what the COMPOSITION emits: the value arrives through an ExternalSecret-materialised Kubernetes Secret and is passed base64 as Kubernetes already stores it, never decoded. The same mechanism is available here, but there are three secrets rather than one, so three requirements and three Secret keys, and the API is ALREADY RELEASED - spec.repository.secure shipped in a tagged version, so its field shapes are a frozen seam and a tightening is a breaking change under the repository’s own rule.

Doing nothing is also an option worth costing: mark the three fields unusable, state the vendor defect and the on-call consequence in the request-schema and catalog docs, and refuse them at admission rather than letting a consumer generate vendor-side load. Refusing a released field is itself breaking.

Acceptance Criteria

Definition of Done

Implementation Plan

Wave 13: test-first refusal of all three repository secure fields at admission and render time, preserve field definitions, and prepare the breaking migration documentation packet.

Admission migration coverage includes all three secure aliases on create and field-addition update, a CRD-upgrade case where each formerly accepted name reference is retained during an unrelated update and refused, and successful removal of each legacy field after the upgrade. Renderer refusal remains independent of admission.

Implementation Notes

2026-09-18 DIRECTION SET BY THE OWNER. Refuse the three fields at admission and document the vendor defect. Do not adopt the create form here.

The reasoning, so it is not re-litigated: the create form would put three credential literals into forProvider where the connection API puts one, and the vendor is expected to fix the reference form, at which point the create-form machinery would be dead weight on a released API. Refusing is the smaller and more reversible change. Adopting the reference form once the vendor fixes it is tracked as its own task.

So the shape of the work is: reject token, webhookSecret and commitSigningKey at admission with a message that names the vendor defect, document it where a consumer of this API reads, and leave the field definitions in place so that re-enabling them later is a rule removal rather than a schema change.

This is a fail-closed tightening on an API released in 2.0.0, so the wave operating model’s rule applies in full: the commit carries a breaking marker and the change gets a migration section. An update that 2.0.0 accepted will be rejected.

AC1 CAVEAT, and it is load-bearing for how the refusal is worded. The 403 on Repository is INFERRED from Connection, a sibling kind in the same vendor API group whose secure-value resolution path is where the refusal was traced. It has not been observed on Repository. Nothing on either live estate has ever created a RepositoryV0Alpha1 with a secure value, so there is no live evidence either way, and probing for it would mean creating a secure value and a repository on a real stack - the same action that raised the vendor’s alert last time, and the vendor’s secret API ignores dryRun, so the probe would be a real mutation. Do not probe as part of this wave. Word the documentation as inferred, name the sibling evidence, and say so plainly rather than asserting an observation that was never made.

Wave 13 terminal evidence: renderer tests refuse token, webhookSecret, and commitSigningKey; admission tests cover create, addition update, retained legacy values after CRD upgrade, and removal. The breaking migration identifies the repository refusal as inferred sibling evidence and preserves the claim-side literal prohibition. Local just check and hosted Validate run 35333659147 passed at b47831ddddfd5ec10e1e699d4d8608886261becd.

Final Summary

Refused all three GrafanaProvisioningRepository secure-value fields at admission and render time, with ratcheting-resistant upgrade behavior and a breaking migration. Documentation distinguishes inferred Repository behavior from the observed sibling Connection failure and explains retry/on-call consequences. Verified by focused renderer and envtest admission tests, the full local gate, and hosted Validate run 35333659147 at b47831ddddfd5ec10e1e699d4d8608886261becd.

View the source file on GitHub