{"record":{"id":"621c0891340a01d2","repo":"Hmbown/CodeWhale","slug":"external-credential-consent-is-scoped-to-provider","errorCode":null,"errorMessage":"external credential consent is scoped to provider {:?}, not {}","messagePattern":"external credential consent is scoped to provider (.+?), not (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/external_credentials.rs","lineNumber":421,"sourceCode":"                \"external credential access is disabled for {}\",\n                provider.as_str()\n            );\n        }\n        if self.access == ExternalCredentialAccess::Managed {\n            bail!(\n                \"managed external credential access is unsupported for {}; no schema-safe preservation adapter is available\",\n                provider.as_str()\n            );\n        }\n        if self.consent_version != EXTERNAL_CREDENTIAL_CONSENT_VERSION {\n            bail!(\n                \"external credential consent for {} uses unsupported version {}; revoke and consent again\",\n                provider.as_str(),\n                self.consent_version\n            );\n        }\n        if self.provider != provider.as_str() {\n            bail!(\n                \"external credential consent is scoped to provider {:?}, not {}\",\n                self.provider,\n                provider.as_str()\n            );\n        }\n        if self.source != source {\n            bail!(\n                \"external credential consent source mismatch for {} (expected {})\",\n                provider.as_str(),\n                source.as_str()\n            );\n        }\n        if !self.path.is_absolute() {\n            bail!(\n                \"external credential consent path for {} must be absolute\",\n                provider.as_str()\n            );\n        }","sourceCodeStart":403,"sourceCodeEnd":439,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/external_credentials.rs#L403-L439","documentation":"Consent records are scoped to exactly one provider; validate_read_scope() compares the stored provider string against the requested ProviderKind and bails when a consent minted for one provider is presented for another. This prevents reusing one provider's file-based credential grant to authorize reads for a different provider.","triggerScenarios":"validate_read_scope(provider_b, ...) is called with a consent record whose `provider` field is \"provider-a\" — e.g. config reuses/copies a consent block but the code path now requests a different provider kind.","commonSituations":"Copy-pasting a consent block in config when adding a second provider and forgetting to update the provider field; provider renames changing ProviderKind::as_str() between versions; tooling that looks up 'any' consent rather than the provider-specific one.","solutions":["Create a separate consent for each provider that should read that file","Fix the provider field in the existing consent to match the provider being authenticated"],"exampleFix":"# before\n[[external_credentials]]\nprovider = \"acme\"\n# ... later used to auth 'contoso'\n\n# after\n[[external_credentials]]\nprovider = \"acme\"\npath = \"/keys/acme.env\"\n[[external_credentials]]\nprovider = \"contoso\"\npath = \"/keys/contoso.env\"","handlingStrategy":"validation","validationCode":"if consent.provider != provider.as_str() {\n    // wrong grant: look up (or create) the consent scoped to this provider\n    consent = find_consent(provider, source, &path).ok_or(missing_consent)?;\n}","typeGuard":"fn matches_provider(consent: &ExternalCredentialConsent, provider: ProviderKind) -> bool {\n    consent.provider == provider.as_str()\n}","tryCatchPattern":"match consent.validate_read_scope(provider, source, &path) {\n    Ok(()) => read_external_credential(&path),\n    Err(e) if e.to_string().contains(\"scoped to provider\") => {\n        // per-provider grant model: consent again for this provider\n        consent_for(provider, source, &path).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Create one consent block per provider; never reuse or copy without editing provider","After provider renames across versions, re-create consents","Key consent lookups by (provider, source), not by path alone"],"tags":["security","consent","provider-scope","credentials"],"backgroundTag":"authorization-scope-mismatch","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}