Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,23 @@ changes since the last release, see the [diff on GitHub][unreleased].
before applying them. Passing `what_if` with an operation that doesn't support it returns an
invalid parameters error.

### Changed

- Extensions that implement the `secret` capability must now define the `args` property for the
`secret` command with exactly one secret name input argument (`nameArg`) and at most one vault
input argument (`vaultArg`). Previously, an extension could omit the secret name input argument,
which left DSC with no way to tell the extension which secret to retrieve. DSC no longer loads an
extension manifest that defines a nonfunctional `secret` command and logs an informational
message explaining the problem. The extension manifest JSON schema enforces the same
requirements.

<details><summary>Related work items</summary>

- Issues: [#1729][#1729]
- PRs: _None_

</details>

## [v3.2.2][release-v3.2.2] - 2026-06-16

This section includes a summary of changes for the `3.2.2` release. For the full list of changes
Expand Down Expand Up @@ -1982,4 +1999,5 @@ Version `3.0.0` is the first generally available release of DSC.
[#1557]: https://github.com/PowerShell/DSC/issues/1557
[#1558]: https://github.com/PowerShell/DSC/issues/1558
[#1562]: https://github.com/PowerShell/DSC/issues/1562
[#1729]: https://github.com/PowerShell/DSC/issues/1729

5 changes: 4 additions & 1 deletion docs/reference/schemas/extension/manifest/root.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,10 @@ runtime. When this property is defined, the extension has the `secret` capabilit
invoke the extension for the [secret()][05] configuration function.

The value of this property must be an object. The object's `executable` property, defining the name
of the command to call, is mandatory. The `args` property is optional. For more information, see
of the command to call, and `args` property, defining the arguments to pass to the command, are
both mandatory. The `args` property must define the secret name input argument exactly once and may
define the vault input argument at most once. DSC doesn't load an extension manifest that defines
the `secret` property without a secret name input argument. For more information, see
[DSC extension manifest secret property schema reference][06].

```yaml
Expand Down
28 changes: 19 additions & 9 deletions docs/reference/schemas/extension/manifest/secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ that extension.
The `secret` definition must include these properties:

- [executable](#executable)
- [args](#args)

## Properties

### executable

The `executable` property defines the name of the command to run. The value must be the name of a
command secretable in the system's `PATH` environment variable or the full path to the command. A
command discoverable in the system's `PATH` environment variable or the full path to the command. A
file extension is only required when the command isn't recognizable by the operating system as an
executable.

Expand All @@ -56,15 +57,22 @@ The `args` property defines the list of arguments to pass to the command. Each i
can be a string representing a static argument, a [name argument](#name-argument) object, or a
[vault argument](#vault-argument) object.

The array should contain exactly one name argument. It may contain a single vault argument and any
The array must contain exactly one name argument. It may contain at most one vault argument and any
number of static string arguments.

If the array doesn't define a name argument, DSC can't pass the secret name to the extension. If
the array doesn't define a vault argument, DSC can't pass the vault name to the extension.
Without a name argument, DSC can't pass the secret name to the extension, so the extension can't
retrieve a specific secret. DSC doesn't load an extension manifest that defines the `secret`
property without the `args` property, without a name argument, with more than one name argument,
or with more than one vault argument. When DSC skips a manifest for one of these reasons, it logs
an informational message that explains the problem. Use the `--trace-level info` option, like
`dsc --trace-level info extension list`, to see the message.

If the array doesn't define a vault argument, DSC can't pass the vault name to the extension, so
the extension can't retrieve a secret from a specific vault.

```yaml
Type: array
Required: false
Required: true
ItemsType: [string, object(Name or Vault argument)]
```

Expand All @@ -79,11 +87,12 @@ Type: string

#### Name argument

Defines an argument that receives the path to the file to import.
Defines the argument that receives the name of the secret to retrieve.

DSC passes the value of `nameArg` followed by the name of the secret to retrieve.
DSC passes the value of `nameArg` followed by the name of the secret to retrieve. The `args` array
must define this argument exactly once.

A file argument is defined as a JSON object with the following properties:
A name argument is defined as a JSON object with the following properties:

- `nameArg` (required) - The argument to pass before the secret name, like `--secret-name`.

Expand All @@ -97,7 +106,8 @@ RequiredProperties: [nameArg]
Defines an argument that receives the name of a specific vault to retrieve a secret from.

DSC passes the value of `vaultArg` followed by the name of the vault when the `secret()` function
specifies a vault. When the function doesn't specify a vault, DSC ignores the vault argument.
specifies a vault. When the function doesn't specify a vault, DSC ignores the vault argument. The
`args` array may define this argument at most once.

A vault argument is defined as a JSON object with the following properties:

Expand Down
62 changes: 62 additions & 0 deletions dsc/tests/dsc_extension_secret.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,66 @@ Describe 'Tests for the secret() function and extensions' {
$env:DSC_RESTRICTED_PATH = $null
}
}

It 'Secret extension manifest <reason> is not loaded' -TestCases @(
@{ reason = 'without args'; secret = '{ "executable": "dsctest" }'; expectedError = 'missing field *args*' }
@{ reason = 'without a name argument'; secret = '{ "executable": "dsctest", "args": ["no-op"] }'; expectedError = "The 'secret' command doesn't define the secret name input argument" }
@{ reason = 'with multiple name arguments'; secret = '{ "executable": "dsctest", "args": ["no-op", { "nameArg": "--name" }, { "nameArg": "--secret" }] }'; expectedError = "The 'secret' command defines the secret name input argument 2 times" }
@{ reason = 'with multiple vault arguments'; secret = '{ "executable": "dsctest", "args": ["no-op", { "nameArg": "--name" }, { "vaultArg": "--vault" }, { "vaultArg": "--store" }] }'; expectedError = "The 'secret' command defines the vault input argument 2 times" }
) {
param($secret, $expectedError)

$manifest = @"
{
"`$schema": "https://aka.ms/dsc/schemas/v3/bundled/extension/manifest.json",
"type": "Test/SecretInvalid",
"version": "0.1.0",
"description": "Invalid secret extension for testing.",
"secret": $secret
}
"@

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
}
}

It 'Secret extension manifest with a name argument and a vault argument is loaded' {
$manifest = @'
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/extension/manifest.json",
"type": "Test/SecretValid",
"version": "0.1.0",
"description": "Valid secret extension for testing.",
"secret": {
"executable": "dsctest",
"args": [
"no-op",
{ "vaultArg": "--vault" },
{ "nameArg": "--name" }
]
}
}
'@

try {
$env:DSC_RESTRICTED_PATH = $TestDrive
Set-Content -Path "$TestDrive/secretValid.dsc.extension.json" -Value $manifest
$out = dsc extension list 2> $TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/error.log)
@($out).Count | Should -Be 1
$out.type | Should -BeExactly 'Test/SecretValid'
$out.capabilities | Should -BeExactly @('secret')
} finally {
$env:DSC_RESTRICTED_PATH = $null
}
}
}
6 changes: 6 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ adaptedResourceFound = "Adapted resource '%{resource}' version %{version} found"
executableNotFound = "Executable '%{executable}' not found for operation '%{operation}' for resource '%{resource}'"
invalidResourceManifest = "Invalid manifest for resource '%{resource}': %{err}"
invalidExtensionManifest = "Invalid manifest for extension '%{resource}': %{err}"
invalidSecretExtensionManifest = "Invalid 'secret' definition for extension '%{extension}' in manifest '%{path}': %{err}"
invalidAdaptedResourceManifest = "Invalid manifest for adapted resource '%{resource}': %{err}"
invalidManifestList = "Invalid manifest list '%{resource}': %{err}"
invalidManifestFile = "Invalid manifest file '%{resource}': %{err}"
Expand Down Expand Up @@ -303,6 +304,11 @@ manifestImported = "Manifest imported from extension %{extension}"
extensionManifestSchemaTitle = "Extension manifest schema URI"
extensionManifestSchemaDescription = "Defines the JSON Schema the extension manifest adheres to."

[extensions.secret]
missingNameArg = "The 'secret' command doesn't define the secret name input argument, so DSC can't pass the name of the secret to retrieve to the extension. Define exactly one argument in 'secret.args' as an object with the 'nameArg' property"
multipleNameArgs = "The 'secret' command defines the secret name input argument %{count} times. Define exactly one argument in 'secret.args' as an object with the 'nameArg' property"
multipleVaultArgs = "The 'secret' command defines the vault input argument %{count} times. Define at most one argument in 'secret.args' as an object with the 'vaultArg' property"

[functions]
invalidArgType = "Invalid argument type"
invalidArguments = "Invalid argument(s)"
Expand Down
3 changes: 3 additions & 0 deletions lib/dsc-lib/src/discovery/command_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,9 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result<
capabilities.push(dscextension::Capability::Discover);
}
if let Some(secret) = &manifest.secret {
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()));
Comment on lines +1000 to +1001
}
verify_executable(&manifest.r#type, "secret", &secret.executable, path.parent().unwrap());
capabilities.push(dscextension::Capability::Secret);
}
Expand Down
Loading
Loading