Repository navigation
(GH-1729) Make nonfunctional secret extension manifests invalid - #1745
Gijs Reijn (Gijsreyn) wants to merge 1 commit into
Conversation
9604a96 to
773af9b
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Validation currently permits an ambiguous argument object, can discard entire manifest lists, and has diagnostic, localization, and test-isolation issues.
5 open findings
Skip invalid manifest entries without discarding valid resources · New Preserve extension type for missing args diagnostics · New Reject unknown fields in untagged argument variants · New Restore previous DSC_RESTRICTED_PATH value · New Localize hard-coded schema titles and validation messages · New
What changed in this PR
Strengthens secret-extension manifests by enforcing required name/vault argument constraints during schema validation, discovery, and invocation.
Changes:
- Adds secret argument validation and schema constraints.
- Updates test tooling, manifests, and automated coverage.
- Documents the stricter contract and compatibility impact.
| File | Description |
|---|---|
tools/dsctest/src/main.rs |
Ignores no-op arguments. |
tools/dsctest/src/args.rs |
Allows trailing no-op arguments. |
tools/dsctest/deprecated/deprecated.dsc.manifests.json |
Fixes deprecated secret manifest. |
lib/dsc-lib/src/extensions/secret.rs |
Implements validation, invocation safeguards, schema rules, and tests. |
lib/dsc-lib/src/discovery/command_discovery.rs |
Rejects invalid secret extensions during discovery. |
lib/dsc-lib/locales/en-us.toml |
Adds validation diagnostics. |
dsc/tests/dsc_extension_secret.tests.ps1 |
Tests valid and invalid manifests. |
docs/reference/schemas/extension/manifest/secret.md |
Documents strict secret arguments. |
docs/reference/schemas/extension/manifest/root.md |
Updates root manifest guidance. |
CHANGELOG.md |
Records the breaking validation change. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
| if let Err(err) = secret.validate_args() { | ||
| return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.invalidSecretExtensionManifest", extension = manifest.r#type, path = path.to_string_lossy(), err = err).to_string())); |
| /// The arguments to pass to the command to retrieve a secret. The arguments must define the | ||
| /// secret name input argument exactly once and may define the vault input argument at most | ||
| /// once. | ||
| pub args: Vec<SecretArgKind>, |
| let name_arg_count = self.args.iter().filter(|arg| matches!(arg, SecretArgKind::Name { .. })).count(); | ||
| let vault_arg_count = self.args.iter().filter(|arg| matches!(arg, SecretArgKind::Vault { .. })).count(); |
| try { | ||
| $env:DSC_RESTRICTED_PATH = $TestDrive | ||
| Set-Content -Path "$TestDrive/secretInvalid.dsc.extension.json" -Value $manifest | ||
| $out = dsc -l info extension list 2> $TestDrive/error.log | ConvertFrom-Json | ||
| $errorLog = Get-Content -Raw -Path $TestDrive/error.log | ||
| $LASTEXITCODE | Should -Be 0 -Because $errorLog | ||
| @($out).type | Should -Not -Contain 'Test/SecretInvalid' | ||
| $errorLog | Should -BeLike "*INFO Failed to load manifest: *$expectedError*" -Because $errorLog | ||
| } finally { | ||
| $env:DSC_RESTRICTED_PATH = $null | ||
| } |
| /// | ||
| /// The subschemas are defined separately so that each failure reports a specific message. | ||
| /// The `errorMessage` keyword is only emitted in the VS Code form of the schema. | ||
| fn transform_args_constraints(schema: &mut Schema) { |
Mikey Lombardi (He/Him) (michaeltlombardi)
left a comment
There was a problem hiding this comment.
Added a few notes on addressing the schema change.
Another option to consider is to define a newtype wrapper around Vec<SecretArgKind>, like (not tested, just quickly scribbled out):
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(try_from = "Vec<serde_json::Value>")]
#[schemars(inline, !try_from)]
pub struct SecretArgs(Vec<SecretArgKind>);
impl SecretArgs {
pub fn from_values(values: Vec<serde_json::Value>) -> Result<Self, DscError> {
Self::validate(&values)?;
// Construction logic
}
fn validate(values: &Vec<serde_json::Value>) -> Result<(), DscError> {
// Validation logic for an array of values
}
}
impl TryFrom<Vec<serde_json::Value>> for SecretArgs {
type Error = DscError;
fn try_from(values: Vec<serde_json::Value>) -> Result<Self, Self::Error> {
SecretArgs::from_values(values)
}
}Which lets us pull the validation into the deserialization process and provide meaningful messages. Generally, we want to raise an error when we can't deserialize data because it fails constraints - this way we can just use the deserialized data in the rest of the code without needing to validate it repeatedly.
| /// Adds the validation subschemas that require `args` to define the secret name input | ||
| /// argument exactly once and the vault input argument at most once. | ||
| /// | ||
| /// The subschemas are defined separately so that each failure reports a specific message. | ||
| /// The `errorMessage` keyword is only emitted in the VS Code form of the schema. | ||
| fn transform_args_constraints(schema: &mut Schema) { | ||
| let docs_url = "https://learn.microsoft.com/powershell/dsc/reference/schemas/extension/manifest/secret"; | ||
| schema.insert("allOf".to_string(), json!([ | ||
| { | ||
| "title": "Missing secret name input argument", | ||
| "properties": { | ||
| "args": { | ||
| "errorMessage": format!( | ||
| "The `secret` command doesn't define the secret name input argument. If you don't define the secret name input argument, DSC can't pass the secret name to the extension for retrieval. You must define exactly one argument in `secret.args` as a JSON object with the `nameArg` property. For more information, see: {docs_url}" | ||
| ), | ||
| "contains": { "type": "object", "required": ["nameArg"] }, | ||
| "minContains": 1 | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "title": "Multiple secret name input arguments", | ||
| "properties": { | ||
| "args": { | ||
| "errorMessage": format!( | ||
| "The `secret` command defines the secret name input argument more than once. You must define exactly one argument in `secret.args` as a JSON object with the `nameArg` property and remove the additional secret name input arguments. For more information, see: {docs_url}" | ||
| ), | ||
| "contains": { "type": "object", "required": ["nameArg"] }, | ||
| "maxContains": 1 | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "title": "Multiple vault input arguments", | ||
| "properties": { | ||
| "args": { | ||
| "errorMessage": format!( | ||
| "The `secret` command defines the vault input argument more than once. You can define at most one argument in `secret.args` as a JSON object with the `vaultArg` property. For more information, see: {docs_url}" | ||
| ), | ||
| "contains": { "type": "object", "required": ["vaultArg"] }, | ||
| "minContains": 0, | ||
| "maxContains": 1 | ||
| } | ||
| } | ||
| } | ||
| ])); | ||
| } |
There was a problem hiding this comment.
Recommend removing this function in favor of defining the constraints directly on the struct with the schemars attribute.
| /// Adds the validation subschemas that require `args` to define the secret name input | |
| /// argument exactly once and the vault input argument at most once. | |
| /// | |
| /// The subschemas are defined separately so that each failure reports a specific message. | |
| /// The `errorMessage` keyword is only emitted in the VS Code form of the schema. | |
| fn transform_args_constraints(schema: &mut Schema) { | |
| let docs_url = "https://learn.microsoft.com/powershell/dsc/reference/schemas/extension/manifest/secret"; | |
| schema.insert("allOf".to_string(), json!([ | |
| { | |
| "title": "Missing secret name input argument", | |
| "properties": { | |
| "args": { | |
| "errorMessage": format!( | |
| "The `secret` command doesn't define the secret name input argument. If you don't define the secret name input argument, DSC can't pass the secret name to the extension for retrieval. You must define exactly one argument in `secret.args` as a JSON object with the `nameArg` property. For more information, see: {docs_url}" | |
| ), | |
| "contains": { "type": "object", "required": ["nameArg"] }, | |
| "minContains": 1 | |
| } | |
| } | |
| }, | |
| { | |
| "title": "Multiple secret name input arguments", | |
| "properties": { | |
| "args": { | |
| "errorMessage": format!( | |
| "The `secret` command defines the secret name input argument more than once. You must define exactly one argument in `secret.args` as a JSON object with the `nameArg` property and remove the additional secret name input arguments. For more information, see: {docs_url}" | |
| ), | |
| "contains": { "type": "object", "required": ["nameArg"] }, | |
| "maxContains": 1 | |
| } | |
| } | |
| }, | |
| { | |
| "title": "Multiple vault input arguments", | |
| "properties": { | |
| "args": { | |
| "errorMessage": format!( | |
| "The `secret` command defines the vault input argument more than once. You can define at most one argument in `secret.args` as a JSON object with the `vaultArg` property. For more information, see: {docs_url}" | |
| ), | |
| "contains": { "type": "object", "required": ["vaultArg"] }, | |
| "minContains": 0, | |
| "maxContains": 1 | |
| } | |
| } | |
| } | |
| ])); | |
| } |
| /// The arguments to pass to the command to retrieve a secret. The arguments must define the | ||
| /// secret name input argument exactly once and may define the vault input argument at most | ||
| /// once. | ||
| pub args: Vec<SecretArgKind>, |
There was a problem hiding this comment.
Recommend defining the static constraints with the schemars attribute directly on the field:
| pub args: Vec<SecretArgKind>, | |
| #[schemars(extend("allOf" = [ | |
| { | |
| "title": schema_i18n!("args.constraints.nameArg.title"), | |
| "description": schema_i18n!("args.constraints.nameArg.description"), | |
| "markdownDescription": schema_i18n!("args.constraints.nameArg.markdownDescription"), | |
| "errorMessage": schema_i18n!("args.constraints.nameArg.errorMessage"), | |
| "contains": { "type": "object", "required": ["nameArg"] }, | |
| "minContains": 1, | |
| "maxContains": 1, | |
| }, | |
| { | |
| "title": schema_i18n!("args.constraints.vaultArg.title"), | |
| "description": schema_i18n!("args.constraints.vaultArg.description"), | |
| "markdownDescription": schema_i18n!("args.constraints.vaultArg.markdownDescription"), | |
| "errorMessage": schema_i18n!("args.constraints.vaultArg.errorMessage"), | |
| "contains": { "type": "object", "required": ["vaultArg"] }, | |
| "minContains": 0, | |
| "maxContains": 1, | |
| }, | |
| ]))] | |
| pub args: Vec<SecretArgKind>, |
Prefer as few constraints as possible for validation - while we can get better error messages by splitting min/max for the name argument, this requires an additional validation pass (one per object in allOf) and requires the schema reader to check multiple constraints that are idiomatically represented as a single constraint.
773af9b to
6fcd53d
Compare


PR Summary
This change:
secretcommand to also define itsargs, with exactly one secret name input argument (nameArg) and at most one vault input argument (vaultArg).secretcommand, so editors flag the problem while the manifest is being written. The VS Code form of the schema carries a specific error message for each rule.no-opsubcommand ofdsctestto accept and ignore arguments, and fixes the deprecated test extension, which was itself a nonfunctional secret extension.PR Context
Prior to this change, the
argsfield for thesecretcommand was optional, and nothing checked whether it contained the secret name input argument. An extension could define asecretcommand that DSC was happy to invoke but could never tell which secret to retrieve: the name was silently dropped from the argument list, and whatever the extension wrote to stdout was treated as the secret. Nothing in the output ofdsc extension listor in the trace messages told the author that something was wrong.The hand-written YAML schema source under
schemas/srcalready described the intended contract:argsrequired, exactly one name argument, at most one vault argument. The published schemas are now generated from the Rust types, though, and those carried none of these constraints. The repository's own deprecated test extension shows how easy it was to get wrong: itssecretcommand passed onlyno-optodsctestand nothing else.The working group chose the first option from the issue, making the contract strict and treating a missing name argument as an error, rather than the second option of appending the secret name as a trailing argument.
The check lives in the loader that turns an extension manifest into a discovered extension, next to the existing checks for the other commands. When it fails, the manifest is rejected the same way as a manifest that doesn't parse, so the behavior is consistent for extension authors: the extension doesn't appear in the list, and running
dsc --trace-level info extension listshows why.Remark on compatibility
This is a breaking change for extension manifests that omit
argsor the name argument. Those manifests loaded before, but the resulting extension couldn't retrieve a specific secret, so the change only excludes definitions that never worked as intended. The Azure CLI example and the test extensions in this repository were already compliant.Remark on the message level
The skipped-manifest message is logged at the info level, matching every other invalid manifest. If this case should be louder, a warning from the loader is a one-line change.
Remark on the published schemas
The checked-in schemas under
schemas/v3aren't regenerated in this change. They don't yet contain thesecretorimportcommand schemas at all, so regenerating them appears to be a separate release-time step rather than something each change is expected to do.