fix(#191): validate credential-template provider so an unknown value can't silently inject nothing - #199
Conversation
…n set
A credential template accepted any string as `provider`. Only a handful of
literals are ever matched when credentials are resolved (`aws`, `ibm`, `gcp`,
`azure`, `ssh`), so a natural misspelling like `provider="ibmcloud"` — which
matches every adjacent field name (`ibmcloud_api_key`, `ibmcloud_resource_group`)
— was stored happily, read back looking healthy, and then matched NO branch in
the resolver. The template silently contributed nothing, the deploy fell through
to global `.env` credentials that weren't there, and the failure surfaced far
away as an opaque Terraform "BearerToken property is required" error.
Root cause: `provider` was never validated at create/update, and the canonical
set it must belong to was implicit, scattered across the resolver's `if
template.provider == ...` branches.
Fix:
- Add `SUPPORTED_PROVIDERS = {aws, gcp, azure, ibm, ssh}` as the single source
of truth in credential_template_service.py, documented against each consumer
that injects/resolves credentials for that provider.
- Validate `provider` in `create_template` and `update_template` (service layer,
covers every caller) -> BadRequestError with an enumerated message.
- Add matching Pydantic validation on the create/update route models so the API
boundary returns a clean 422 naming the bad value and the supported set,
before the service is reached. Update-time validation only fires when the
caller is actually changing `provider`.
Tests lock: the exact `ibmcloud` misspelling is rejected at both the service
(400) and route (422) layers; every canonical provider is still accepted; an
update can't switch a template onto a no-op provider; and a resolver-contract
test documents that a pre-existing `ibmcloud` row injects nothing (the behavior
the validation now prevents from being created). Mutation-tested: reverting the
guards reds 5 tests.
Closes #191
Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
Self-review M3: the SUPPORTED_PROVIDERS comment overstated azure/gcp as general 'credential resolution' — they inject only a post-provision kubeconfig token (AKS via engine_router, GKE via get_gcp_service_account_info), not terraform-env credentials. Only aws/ibm inject into the terraform env, so #191's 'looks healthy, injects nothing' class fully closes for aws/ibm; azure/gcp are still validated but that terraform-env class never applied to them. Comment-only; no behavior change. Claude-Session: https://claude.ai/code/session_01UpRYiFserdBE5ESHn759N4
Self-review (cold, adversarial) — verdict: no blocker, no majorAn independent cold auditor reviewed this PR against issue #191, executing the code rather than eyeballing. Full affected suite: 108 passed; the guard tests were mutation-checked (neutralize Held under attack:
Findings (all minor/informational):
Net: the fix is sound and non-vacuous; the one actionable item (M3 comment) is fixed. |
Review discipline pass — verdict: REVISETwo independent cold audits (clean context, no prior review threads) plus an invariant sweep. Everything below was verified by execution, not inspection. Reviewed at What holds up
Major1. Constrain the field type instead of validating it in a A model-level To be clear about scope: this PR does not introduce that mechanism — the pre-existing IBM-key-required raise at Typing the field closes it — verified: the error becomes field-scoped ( 2. INV-3: the constraint never reaches the API contract. The repo's own type-generation strategy maps
Consequences: Findings 1 and 2 share one fix, which deletes lines rather than adding them: # CredentialTemplateBase
provider: Literal["aws", "azure", "gcp", "ibm", "ssh"]
# CredentialTemplateUpdate
provider: Literal["aws", "azure", "gcp", "ibm", "ssh"] | None = Nonethen regenerate Minor3. The comment at if "provider" in update_data:
validate_provider(update_data["provider"])Worth noting the published contract advertises null as valid here: 4. The rule is expressed three times and the messages have already diverged. 5. Issue #191's own row is neither repaired nor surfaced. No migration, backfill, or detection. A stored 6. Vacuous assertion. Nits
Out of scope — worth its own issue
Verdict REVISE, not BLOCK. The fix is correct, the tests are real, and #191's create path genuinely closes. Findings 1 and 2 collapse into a single type change that is already this codebase's idiom. |
Summary
POST/PUT /api/credential-templatesaccepted any string asprovider. Credential resolution only ever matches a small set of literals (aws,ibm,gcp,azure,ssh), so a natural misspelling likeprovider="ibmcloud"was stored, read back looking healthy, and then matched no branch in the resolver — the template silently injected nothing and the deploy fell through to global.envcreds. The failure surfaced far away as an opaque Terraform"BearerToken property is required"error.Root cause
providerwas never validated at create/update, and the canonical set it must belong to was implicit — scattered across the resolver'sif template.provider == ...branches.Fix
SUPPORTED_PROVIDERS = {aws, gcp, azure, ibm, ssh}as the single source of truth incredential_template_service.py, documented against each consumer that injects/resolves credentials (AWS/IBM env injection, GCP SA JSON, Azure engine_router, SSH tunnel). This is exactly the four cloud providers the UI offers plus the legacysshprovider.providerincreate_templateandupdate_template(service layer — covers every caller) →BadRequestError(400) with an enumerated message.provideris actually being changed.What the tests lock
ibmcloudmisspelling from the report is rejected at both the service (400) and route (422) layers, with an enumerated message.aws,gcp,azure,ibm,ssh) is still accepted.provideron update leaves it untouched.ibmcloudrow injects no IBM credentials — the exact silent no-op the validation now prevents from being created.Verification:
pyteston the three affected test files → 108 passed;ruff checkon all changed files → clean.Closes #191