diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index 48ee20782..5751be8d1 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -356,7 +356,8 @@ jobs: files=$(ls packages/*.nupkg | jq -R -s -c ' split("\n")[:-1] | map(select( - (test("/Dapr\\.Workflow\\.Versioning\\.(Abstractions|Generators|Runtime)\\.")) | not + ((test("/Dapr\\.Workflow\\.Versioning\\.(Abstractions|Generators|Runtime)\\.")) | not) and + ((test("/Dapr\\.SecretsManagement\\.(Abstractions|Generators|Runtime)\\.")) | not) )) ') echo "matrix=$files" >> $GITHUB_OUTPUT diff --git a/all.sln b/all.sln index 47e798512..91cfaef72 100644 --- a/all.sln +++ b/all.sln @@ -260,6 +260,19 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Actors EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowRetryPolicy", "examples\Workflow\WorkflowRetryPolicy\WorkflowRetryPolicy.csproj", "{6C77A2C4-0A96-4B90-AFEA-16E86A906259}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.SecretsManagement.Abstractions", "src\Dapr.SecretsManagement.Abstractions\Dapr.SecretsManagement.Abstractions.csproj", "{366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.SecretsManagement.Runtime", "src\Dapr.SecretsManagement.Runtime\Dapr.SecretsManagement.Runtime.csproj", "{F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.SecretsManagement.Generators", "src\Dapr.SecretsManagement.Generators\Dapr.SecretsManagement.Generators.csproj", "{B42DD6AA-255C-4606-8A1B-263B26650DED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.SecretsManagement", "src\Dapr.SecretsManagement\Dapr.SecretsManagement.csproj", "{1BECAC48-1C83-43D2-A91F-9AFA634A68C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.SecretsManagement.Runtime.Test", "test\Dapr.SecretsManagement.Runtime.Test\Dapr.SecretsManagement.Runtime.Test.csproj", "{87842296-C78B-42B9-8E96-5054E79CD700}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SecretManagement", "SecretManagement", "{929A8AD2-DB45-B92A-7930-EBDD2DBAF802}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecretManagementSample", "examples\SecretManagement\SecretManagementSample\SecretManagementSample.csproj", "{ED74B33F-3CE7-42EB-BAA1-623F43900B15}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI.Microsoft.Extensions.Test", "test\Dapr.AI.Microsoft.Extensions.Test\Dapr.AI.Microsoft.Extensions.Test.csproj", "{86CBB08F-601A-4B0B-87BF-383D391A961C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.DistributedLock.Test", "test\Dapr.DistributedLock.Test\Dapr.DistributedLock.Test.csproj", "{2B8E9CAD-F9A2-43B9-BB1C-619CF64476A0}" @@ -1498,6 +1511,78 @@ Global {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Debug|x64.ActiveCfg = Debug|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Debug|x64.Build.0 = Debug|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Debug|x86.ActiveCfg = Debug|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Debug|x86.Build.0 = Debug|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Release|Any CPU.Build.0 = Release|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Release|x64.ActiveCfg = Release|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Release|x64.Build.0 = Release|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Release|x86.ActiveCfg = Release|Any CPU + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E}.Release|x86.Build.0 = Release|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Debug|x64.Build.0 = Debug|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Debug|x86.Build.0 = Debug|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Release|Any CPU.Build.0 = Release|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Release|x64.ActiveCfg = Release|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Release|x64.Build.0 = Release|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Release|x86.ActiveCfg = Release|Any CPU + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2}.Release|x86.Build.0 = Release|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Debug|x64.ActiveCfg = Debug|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Debug|x64.Build.0 = Debug|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Debug|x86.ActiveCfg = Debug|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Debug|x86.Build.0 = Debug|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Release|Any CPU.Build.0 = Release|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Release|x64.ActiveCfg = Release|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Release|x64.Build.0 = Release|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Release|x86.ActiveCfg = Release|Any CPU + {B42DD6AA-255C-4606-8A1B-263B26650DED}.Release|x86.Build.0 = Release|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Debug|x64.Build.0 = Debug|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Debug|x86.Build.0 = Debug|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Release|Any CPU.Build.0 = Release|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Release|x64.ActiveCfg = Release|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Release|x64.Build.0 = Release|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Release|x86.ActiveCfg = Release|Any CPU + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7}.Release|x86.Build.0 = Release|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Debug|x64.ActiveCfg = Debug|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Debug|x64.Build.0 = Debug|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Debug|x86.ActiveCfg = Debug|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Debug|x86.Build.0 = Debug|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Release|Any CPU.Build.0 = Release|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Release|x64.ActiveCfg = Release|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Release|x64.Build.0 = Release|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Release|x86.ActiveCfg = Release|Any CPU + {87842296-C78B-42B9-8E96-5054E79CD700}.Release|x86.Build.0 = Release|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Debug|x64.Build.0 = Debug|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Debug|x86.Build.0 = Debug|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Release|Any CPU.Build.0 = Release|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Release|x64.ActiveCfg = Release|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Release|x64.Build.0 = Release|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Release|x86.ActiveCfg = Release|Any CPU + {ED74B33F-3CE7-42EB-BAA1-623F43900B15}.Release|x86.Build.0 = Release|Any CPU {8777EAD2-419B-4683-826A-82B7C1F4F69F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8777EAD2-419B-4683-826A-82B7C1F4F69F}.Debug|Any CPU.Build.0 = Debug|Any CPU {8777EAD2-419B-4683-826A-82B7C1F4F69F}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -1681,6 +1766,13 @@ Global {01A20A89-53A1-4D5B-B563-89E157718474} = {8462B106-175A-423A-BA94-BE0D39D0BD8E} {7B14879F-156B-417E-ACA3-0B5A69CC2F39} = {8462B106-175A-423A-BA94-BE0D39D0BD8E} {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {8462B106-175A-423A-BA94-BE0D39D0BD8E} + {366FA402-B13D-4EE2-9BA4-A0C3134C6C6E} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {F99CE5A1-FDA1-415C-B1E6-C8787734ACD2} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {B42DD6AA-255C-4606-8A1B-263B26650DED} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {1BECAC48-1C83-43D2-A91F-9AFA634A68C7} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {87842296-C78B-42B9-8E96-5054E79CD700} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {929A8AD2-DB45-B92A-7930-EBDD2DBAF802} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {ED74B33F-3CE7-42EB-BAA1-623F43900B15} = {929A8AD2-DB45-B92A-7930-EBDD2DBAF802} {6C77A2C4-0A96-4B90-AFEA-16E86A906259} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {86CBB08F-601A-4B0B-87BF-383D391A961C} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD} {2B8E9CAD-F9A2-43B9-BB1C-619CF64476A0} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD} diff --git a/examples/SecretManagement/README.md b/examples/SecretManagement/README.md new file mode 100644 index 000000000..9e523c8ba --- /dev/null +++ b/examples/SecretManagement/README.md @@ -0,0 +1,37 @@ +# Dapr Secrets Management Sample + +This sample demonstrates how to use the Dapr Secrets Management SDK to retrieve secrets from Dapr secret store components. + +## Features Demonstrated + +1. **Direct secret retrieval** — Using `DaprSecretsManagementClient` to fetch individual or bulk secrets via gRPC. +2. **Typed secret stores** — Using the `[SecretStore]` and `[Secret]` attributes with the source generator to create strongly-typed secret accessors. +3. **Dependency injection** — Registering the secrets client and typed stores via `IServiceCollection` extensions. + +## Prerequisites + +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- A configured Dapr secret store component (e.g., local file, Kubernetes secrets, Azure Key Vault) + +## Running the Sample + +```bash +dapr run --app-id secret-sample --app-port 6543 -- dotnet run +``` + +## Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/secrets/{storeName}/{key}` | Retrieve a single secret by key | +| GET | `/secrets/{storeName}` | Retrieve all secrets from a store | +| GET | `/typed-secrets` | Retrieve secrets using the source-generated typed store | + +## NuGet Package Note + +When consuming from NuGet, install the single **`Dapr.SecretsManagement`** package. The sub-projects (`Abstractions`, `Runtime`, `Generators`) are bundled into this one package and are not published individually. + +```xml + +``` diff --git a/examples/SecretManagement/SecretManagementSample/IMyVaultSecrets.cs b/examples/SecretManagement/SecretManagementSample/IMyVaultSecrets.cs new file mode 100644 index 000000000..18eca9b68 --- /dev/null +++ b/examples/SecretManagement/SecretManagementSample/IMyVaultSecrets.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.SecretsManagement.Abstractions; + +namespace SecretManagementSample; + +/// +/// Example of a typed secret store interface. Apply the to an interface +/// and the Dapr Secrets Management source generator will produce: +/// 1. A concrete implementation that caches secrets loaded at startup. +/// 2. A DI registration extension method (e.g., AddMyVaultSecrets()). +/// +/// Properties without use the property name as the secret key. +/// Properties with use the specified secret name. +/// +[SecretStore("my-vault")] +public partial interface IMyVaultSecrets +{ + /// + /// The database connection string, retrieved from the "db-connection-string" secret key. + /// + [Secret("db-connection-string")] + string DatabaseConnection { get; } + + /// + /// The API key. Uses the property name "ApiKey" as the secret key. + /// + string ApiKey { get; } +} diff --git a/examples/SecretManagement/SecretManagementSample/Program.cs b/examples/SecretManagement/SecretManagementSample/Program.cs new file mode 100644 index 000000000..963a5be7b --- /dev/null +++ b/examples/SecretManagement/SecretManagementSample/Program.cs @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.SecretsManagement; +using Dapr.SecretsManagement.Extensions; +using SecretManagementSample; + +var builder = WebApplication.CreateBuilder(args); + +// Register the Dapr Secrets Management client and the source-generated typed secret store. +// AddMyVaultSecrets() is a generated extension method — see IMyVaultSecrets.cs. +builder.Services.AddDaprSecretsManagementClient() + .AddMyVaultSecrets(); + +var app = builder.Build(); + +// --- Example 1: Direct secret retrieval --- +app.MapGet("/secrets/{storeName}/{key}", async ( + string storeName, + string key, + DaprSecretsManagementClient secretsClient, + CancellationToken cancellationToken) => +{ + var secret = await secretsClient.GetSecretAsync(storeName, key, cancellationToken: cancellationToken); + return Results.Ok(secret); +}); + +// --- Example 2: Bulk secret retrieval --- +app.MapGet("/secrets/{storeName}", async ( + string storeName, + DaprSecretsManagementClient secretsClient, + CancellationToken cancellationToken) => +{ + var secrets = await secretsClient.GetBulkSecretAsync(storeName, cancellationToken: cancellationToken); + return Results.Ok(secrets); +}); + +// --- Example 3: Using the source-generated typed secret store --- +app.MapGet("/typed-secrets", (SecretManagementSample.IMyVaultSecrets secrets) => +{ + return Results.Ok(new + { + DatabaseConnection = secrets.DatabaseConnection, + ApiKey = secrets.ApiKey + }); +}); + +app.Run(); diff --git a/examples/SecretManagement/SecretManagementSample/Properties/launchSettings.json b/examples/SecretManagement/SecretManagementSample/Properties/launchSettings.json new file mode 100644 index 000000000..33cfac2df --- /dev/null +++ b/examples/SecretManagement/SecretManagementSample/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "SecretManagementSample": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:6543", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/SecretManagement/SecretManagementSample/SecretManagementSample.csproj b/examples/SecretManagement/SecretManagementSample/SecretManagementSample.csproj new file mode 100644 index 000000000..50dc200bd --- /dev/null +++ b/examples/SecretManagement/SecretManagementSample/SecretManagementSample.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + + + true + $(BaseIntermediateOutputPath)Generated + + + + + + + + + + diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 5875274af..612d2da6a 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -26,6 +26,7 @@ [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Workflow.Grpc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.SecretsManagement.Runtime, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -49,3 +50,4 @@ [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.SecretsManagement.Runtime.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.SecretsManagement.Abstractions/Dapr.SecretsManagement.Abstractions.csproj b/src/Dapr.SecretsManagement.Abstractions/Dapr.SecretsManagement.Abstractions.csproj new file mode 100644 index 000000000..abba95de4 --- /dev/null +++ b/src/Dapr.SecretsManagement.Abstractions/Dapr.SecretsManagement.Abstractions.csproj @@ -0,0 +1,8 @@ + + + + enable + enable + + + diff --git a/src/Dapr.SecretsManagement.Abstractions/SecretAttribute.cs b/src/Dapr.SecretsManagement.Abstractions/SecretAttribute.cs new file mode 100644 index 000000000..211d9256a --- /dev/null +++ b/src/Dapr.SecretsManagement.Abstractions/SecretAttribute.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.SecretsManagement.Abstractions; + +/// +/// Specifies the secret key name used when retrieving the value of the annotated property from a Dapr +/// secret store. When this attribute is omitted, the property name is used as the secret key. +/// +/// +/// This attribute should be applied to properties on an interface that is also annotated with +/// . The source generator uses this metadata to map each property +/// to the correct secret key during bulk secret retrieval. +/// +/// +/// +/// [SecretStore("my-vault")] +/// public partial interface IMySecrets +/// { +/// [Secret("database-connection-string")] +/// string DbConnection { get; } +/// } +/// +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public sealed class SecretAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the secret key in the Dapr secret store. This value is used when calling the + /// Dapr Secrets API to retrieve the secret value. + /// + /// Thrown when is . + public SecretAttribute(string secretName) + { + ArgumentNullException.ThrowIfNull(secretName); + SecretName = secretName; + } + + /// + /// Gets the name of the secret key in the Dapr secret store. + /// + public string SecretName { get; } +} diff --git a/src/Dapr.SecretsManagement.Abstractions/SecretStoreAttribute.cs b/src/Dapr.SecretsManagement.Abstractions/SecretStoreAttribute.cs new file mode 100644 index 000000000..7e8ab73cb --- /dev/null +++ b/src/Dapr.SecretsManagement.Abstractions/SecretStoreAttribute.cs @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.SecretsManagement.Abstractions; + +/// +/// Marks an interface as a typed accessor for a Dapr secret store. When applied to a partial interface, +/// the Dapr Secrets Management source generator will produce a concrete implementation that retrieves secrets +/// from the specified Dapr secret store component and registers it in the dependency injection container. +/// +/// +/// +/// The interface should declare -typed read-only properties. Each property maps to a +/// single secret key in the store. The key name defaults to the property name but can be overridden with +/// . +/// +/// +/// Generated implementations load all mapped secrets in bulk at startup via an IHostedService and +/// expose them as synchronous properties. This makes secrets available immediately after host startup without +/// requiring callers to manage async flows. +/// +/// +/// +/// [SecretStore("my-vault")] +/// public partial interface IMySecrets +/// { +/// /// <summary>The database connection string.</summary> +/// [Secret("db-connection-string")] +/// string DatabaseConnection { get; } +/// +/// /// <summary>The API key (uses property name as secret key).</summary> +/// string ApiKey { get; } +/// } +/// +/// +/// +[AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] +public sealed class SecretStoreAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the Dapr secret store component to retrieve secrets from. This must match the + /// metadata.name of a configured Dapr secret store component. + /// + /// Thrown when is . + public SecretStoreAttribute(string storeName) + { + ArgumentNullException.ThrowIfNull(storeName); + StoreName = storeName; + } + + /// + /// Gets the name of the Dapr secret store component that secrets will be retrieved from. + /// + public string StoreName { get; } +} diff --git a/src/Dapr.SecretsManagement.Abstractions/WellKnownSecrets.cs b/src/Dapr.SecretsManagement.Abstractions/WellKnownSecrets.cs new file mode 100644 index 000000000..909f50303 --- /dev/null +++ b/src/Dapr.SecretsManagement.Abstractions/WellKnownSecrets.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.SecretsManagement.Abstractions; + +/// +/// Well-known names and constants related to Dapr Secrets Management. +/// +internal static class WellKnownSecrets +{ + /// + /// The fully qualified name of . + /// + public const string SecretStoreAttributeFullName = "Dapr.SecretsManagement.Abstractions.SecretStoreAttribute"; + + /// + /// The fully qualified name of . + /// + public const string SecretAttributeFullName = "Dapr.SecretsManagement.Abstractions.SecretAttribute"; +} diff --git a/src/Dapr.SecretsManagement.Generators/Dapr.SecretsManagement.Generators.csproj b/src/Dapr.SecretsManagement.Generators/Dapr.SecretsManagement.Generators.csproj new file mode 100644 index 000000000..063de25a9 --- /dev/null +++ b/src/Dapr.SecretsManagement.Generators/Dapr.SecretsManagement.Generators.csproj @@ -0,0 +1,23 @@ + + + + + netstandard2.0 + false + enable + + true + true + + Dapr.SecretsManagement + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Dapr.SecretsManagement.Generators/KnownSymbols.cs b/src/Dapr.SecretsManagement.Generators/KnownSymbols.cs new file mode 100644 index 000000000..b869974d9 --- /dev/null +++ b/src/Dapr.SecretsManagement.Generators/KnownSymbols.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.CodeAnalysis; + +namespace Dapr.SecretsManagement; + +/// +/// Caches well-known Roslyn symbols used during source generation. +/// +internal sealed record KnownSymbols( + INamedTypeSymbol? SecretStoreAttribute, + INamedTypeSymbol? SecretAttribute); diff --git a/src/Dapr.SecretsManagement.Generators/SecretStoreSourceGenerator.cs b/src/Dapr.SecretsManagement.Generators/SecretStoreSourceGenerator.cs new file mode 100644 index 000000000..89d032f02 --- /dev/null +++ b/src/Dapr.SecretsManagement.Generators/SecretStoreSourceGenerator.cs @@ -0,0 +1,308 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.SecretsManagement; + +/// +/// Incremental source generator that discovers interfaces annotated with [SecretStore], +/// reads their property mappings (including optional [Secret] attributes), and emits: +/// +/// A concrete implementation class that stores secret values retrieved from the Dapr secret store. +/// A DI registration extension method on IDaprSecretsManagementBuilder. +/// +/// +[Generator(LanguageNames.CSharp)] +public sealed class SecretStoreSourceGenerator : IIncrementalGenerator +{ + private const string SecretStoreAttributeFullName = "Dapr.SecretsManagement.Abstractions.SecretStoreAttribute"; + private const string SecretAttributeFullName = "Dapr.SecretsManagement.Abstractions.SecretAttribute"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Cache the attribute symbols. + var known = context.CompilationProvider.Select((c, _) => + new KnownSymbols( + SecretStoreAttribute: c.GetTypeByMetadataName(SecretStoreAttributeFullName), + SecretAttribute: c.GetTypeByMetadataName(SecretAttributeFullName))); + + // Report diagnostic if the attribute types are not found. + context.RegisterSourceOutput(known, (spc, ks) => + { + if (ks.SecretStoreAttribute is null) + { + spc.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor( + "DAPR1601", + "SecretStore attribute not found", + "The source generator could not find the SecretStoreAttribute type. " + + "Ensure that the Dapr.SecretsManagement package is properly referenced.", + "Dapr.SecretsManagement", + DiagnosticSeverity.Warning, + isEnabledByDefault: true), + Location.None)); + } + }); + + // Discover candidate interface declarations with at least one attribute. + var candidates = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is InterfaceDeclarationSyntax ids && ids.AttributeLists.Count > 0, + transform: static (ctx, ct) => + { + var interfaceSyntax = (InterfaceDeclarationSyntax)ctx.Node; + var symbol = ctx.SemanticModel.GetDeclaredSymbol(interfaceSyntax, ct); + return symbol as INamedTypeSymbol; + }) + .Where(static s => s is not null)!; + + // Combine candidates with known symbols and generate. + var combined = candidates.Combine(known); + + context.RegisterSourceOutput(combined, (spc, pair) => + { + var (interfaceSymbol, ks) = pair; + if (ks.SecretStoreAttribute is null || interfaceSymbol is null) + return; + + // Check if the interface has [SecretStore] attribute. + var storeAttrData = interfaceSymbol.GetAttributes().FirstOrDefault(a => + SymbolEqualityComparer.Default.Equals(a.AttributeClass, ks.SecretStoreAttribute)); + + if (storeAttrData is null) + return; + + // Extract the store name from the constructor argument. + if (storeAttrData.ConstructorArguments.Length < 1 || + storeAttrData.ConstructorArguments[0].Value is not string storeName) + return; + + // Collect properties and their secret key mappings. + var properties = new List<(string PropertyName, string SecretKey, string PropertyType)>(); + + foreach (var member in interfaceSymbol.GetMembers()) + { + if (member is not IPropertySymbol prop) + continue; + + if (prop.GetMethod is null) + continue; + + var secretKey = prop.Name; + + // Check for [Secret("key")] override. + if (ks.SecretAttribute is not null) + { + var secretAttrData = prop.GetAttributes().FirstOrDefault(a => + SymbolEqualityComparer.Default.Equals(a.AttributeClass, ks.SecretAttribute)); + + if (secretAttrData is not null && + secretAttrData.ConstructorArguments.Length > 0 && + secretAttrData.ConstructorArguments[0].Value is string overrideName) + { + secretKey = overrideName; + } + } + + properties.Add((prop.Name, secretKey, prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + } + + if (properties.Count == 0) + return; + + // Resolve naming. + var interfaceName = interfaceSymbol.Name; + var namespaceName = interfaceSymbol.ContainingNamespace.IsGlobalNamespace + ? null + : interfaceSymbol.ContainingNamespace.ToDisplayString(); + + // Implementation class name: strip leading 'I' from interface name, append "SecretStoreClient". + var implName = interfaceName.StartsWith("I", StringComparison.Ordinal) && interfaceName.Length > 1 && char.IsUpper(interfaceName[1]) + ? interfaceName.Substring(1) + "SecretStoreClient" + : interfaceName + "SecretStoreClient"; + + var source = GenerateSource( + namespaceName, interfaceName, implName, storeName, + interfaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + properties); + + var fqn = interfaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var hintName = implName + "_" + GetStableHash(fqn).ToString("X8") + ".g.cs"; + spc.AddSource(hintName, source); + }); + } + + private static string GenerateSource( + string? namespaceName, + string interfaceName, + string implName, + string storeName, + string fullyQualifiedInterfaceName, + List<(string PropertyName, string SecretKey, string PropertyType)> properties) + { + var sb = new StringBuilder(); + + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + sb.AppendLine("using System;"); + sb.AppendLine("using System.CodeDom.Compiler;"); + sb.AppendLine("using System.Collections.Generic;"); + sb.AppendLine("using System.Threading;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + sb.AppendLine("using Microsoft.Extensions.Hosting;"); + sb.AppendLine(); + + if (namespaceName is not null) + { + sb.AppendLine($"namespace {namespaceName}"); + sb.AppendLine("{"); + } + + // 1. Implementation class + sb.AppendLine($" /// "); + sb.AppendLine($" /// Auto-generated implementation of that retrieves secrets from the"); + sb.AppendLine($" /// Dapr secret store component named {EscapeXml(storeName)}."); + sb.AppendLine($" /// "); + sb.AppendLine($" [GeneratedCode(\"Dapr.SecretsManagement.Generators\", \"1.0.0\")]"); + sb.AppendLine($" internal sealed class {implName} : {fullyQualifiedInterfaceName}"); + sb.AppendLine(" {"); + sb.AppendLine(" private readonly Dictionary _secrets;"); + sb.AppendLine(); + sb.AppendLine($" internal {implName}(Dictionary secrets)"); + sb.AppendLine(" {"); + sb.AppendLine(" _secrets = secrets ?? throw new ArgumentNullException(nameof(secrets));"); + sb.AppendLine(" }"); + sb.AppendLine(); + + foreach (var (propName, secretKey, propType) in properties) + { + sb.AppendLine($" /// "); + sb.AppendLine($" public {propType} {propName} =>"); + sb.AppendLine($" _secrets.TryGetValue(\"{EscapeCSharpString(secretKey)}\", out var __{propName}Value)"); + sb.AppendLine($" ? __{propName}Value"); + sb.AppendLine($" : throw new KeyNotFoundException(\"Secret '{EscapeCSharpString(secretKey)}' was not found in store '{EscapeCSharpString(storeName)}'.\");"); + sb.AppendLine(); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + + // 2. Hosted service that loads secrets at startup + var loaderName = implName.Replace("SecretStoreClient", "SecretStoreLoader"); + sb.AppendLine($" /// "); + sb.AppendLine($" /// Hosted service that pre-loads secrets from the {EscapeXml(storeName)} Dapr secret store"); + sb.AppendLine($" /// at application startup and registers the generated in the container."); + sb.AppendLine($" /// "); + sb.AppendLine($" [GeneratedCode(\"Dapr.SecretsManagement.Generators\", \"1.0.0\")]"); + sb.AppendLine($" internal sealed class {loaderName} : IHostedService"); + sb.AppendLine(" {"); + sb.AppendLine($" private readonly Dapr.SecretsManagement.DaprSecretsManagementClient _client;"); + sb.AppendLine($" private Dictionary? _secrets;"); + sb.AppendLine(); + sb.AppendLine($" public {loaderName}(Dapr.SecretsManagement.DaprSecretsManagementClient client)"); + sb.AppendLine(" {"); + sb.AppendLine(" _client = client ?? throw new ArgumentNullException(nameof(client));"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" internal Dictionary Secrets =>"); + sb.AppendLine(" _secrets ?? throw new InvalidOperationException("); + sb.AppendLine($" \"Secrets from store '{EscapeCSharpString(storeName)}' have not been loaded yet. Ensure the host has started.\");"); + sb.AppendLine(); + sb.AppendLine(" public async Task StartAsync(CancellationToken cancellationToken)"); + sb.AppendLine(" {"); + sb.AppendLine($" var bulk = await _client.GetBulkSecretAsync(\"{EscapeCSharpString(storeName)}\", cancellationToken: cancellationToken).ConfigureAwait(false);"); + sb.AppendLine(" var flat = new Dictionary();"); + sb.AppendLine(" foreach (var entry in bulk)"); + sb.AppendLine(" {"); + sb.AppendLine(" foreach (var kvp in entry.Value)"); + sb.AppendLine(" {"); + sb.AppendLine(" flat[kvp.Key] = kvp.Value;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(" _secrets = flat;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + // 3. DI registration extension + var methodName = "Add" + (interfaceName.StartsWith("I", StringComparison.Ordinal) && interfaceName.Length > 1 && char.IsUpper(interfaceName[1]) + ? interfaceName.Substring(1) + : interfaceName); + + sb.AppendLine($" /// "); + sb.AppendLine($" /// Extension methods for registering the generated implementation."); + sb.AppendLine($" /// "); + sb.AppendLine($" [GeneratedCode(\"Dapr.SecretsManagement.Generators\", \"1.0.0\")]"); + sb.AppendLine($" public static class {implName}Extensions"); + sb.AppendLine(" {"); + sb.AppendLine($" /// "); + sb.AppendLine($" /// Registers the generated typed secret store implementation for "); + sb.AppendLine($" /// with the dependency injection container. Secrets are loaded from the {EscapeXml(storeName)}"); + sb.AppendLine($" /// Dapr secret store at application startup via an ."); + sb.AppendLine($" /// "); + sb.AppendLine($" /// The Dapr Secrets Management builder."); + sb.AppendLine($" /// The builder instance for chaining."); + sb.AppendLine($" public static Dapr.SecretsManagement.IDaprSecretsManagementBuilder {methodName}("); + sb.AppendLine($" this Dapr.SecretsManagement.IDaprSecretsManagementBuilder builder)"); + sb.AppendLine(" {"); + sb.AppendLine($" builder.Services.AddSingleton<{loaderName}>();"); + sb.AppendLine($" builder.Services.AddSingleton(sp => sp.GetRequiredService<{loaderName}>());"); + sb.AppendLine($" builder.Services.AddSingleton<{fullyQualifiedInterfaceName}>(sp =>"); + sb.AppendLine($" new {implName}(sp.GetRequiredService<{loaderName}>().Secrets));"); + sb.AppendLine(" return builder;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + + if (namespaceName is not null) + { + sb.AppendLine("}"); + } + + return sb.ToString(); + } + + private static string EscapeCSharpString(string value) => + value.Replace("\\", "\\\\").Replace("\"", "\\\""); + + private static string EscapeXml(string value) => + value.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + + /// + /// Produces a stable, deterministic 32-bit hash from the given string. + /// Used to generate unique hint names when multiple types share the same simple name + /// across different namespaces. + /// + private static uint GetStableHash(string text) + { + unchecked + { + uint hash = 2166136261; + foreach (var ch in text) + { + hash = (hash ^ ch) * 16777619; + } + return hash; + } + } +} diff --git a/src/Dapr.SecretsManagement.Runtime/AssemblyInfo.cs b/src/Dapr.SecretsManagement.Runtime/AssemblyInfo.cs new file mode 100644 index 000000000..b6f1304b3 --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.SecretsManagement.Runtime.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.SecretsManagement.Runtime/Dapr.SecretsManagement.Runtime.csproj b/src/Dapr.SecretsManagement.Runtime/Dapr.SecretsManagement.Runtime.csproj new file mode 100644 index 000000000..a9648a8e2 --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/Dapr.SecretsManagement.Runtime.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + + + + + + + + + + + + + + + + diff --git a/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementClient.cs b/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementClient.cs new file mode 100644 index 000000000..335d7a8e5 --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementClient.cs @@ -0,0 +1,120 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; + +namespace Dapr.SecretsManagement; + +/// +/// +/// Defines client operations for managing Dapr secrets. Use +/// to create a or register for use with dependency injection via +/// . +/// +/// +/// Implementations of implement because the +/// client accesses network resources. For best performance, create a single long-lived client instance and share +/// it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid creating +/// and disposing a client instance for each operation that the application performs — this can lead to socket +/// exhaustion and other problems. +/// +/// +public abstract class DaprSecretsManagementClient( + Autogenerated.DaprClient client, + HttpClient httpClient, + string? daprApiToken = null) : IDaprClient +{ + private bool disposed; + + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient = httpClient; + + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken = daprApiToken; + + /// + /// The autogenerated Dapr gRPC client. + /// + /// + /// Property exposed for testing purposes. + /// + internal Autogenerated.DaprClient Client { get; } = client; + + /// + /// Gets the secret value for a specific key from the specified secret store. + /// + /// The name of the Dapr secret store component. + /// The secret key to retrieve. + /// + /// An optional collection of metadata key-value pairs that will be provided to the secret store component. + /// The valid metadata keys and values are determined by the type of secret store used. + /// + /// A that can be used to cancel the operation. + /// + /// A that resolves to a containing the secret + /// data. Some secret stores (such as Kubernetes) can store multiple values per key — each entry in the + /// dictionary represents one such value. + /// + public abstract Task> GetSecretAsync( + string storeName, + string key, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); + + /// + /// Gets all secret values that the application is allowed to access from the specified secret store. + /// + /// The name of the Dapr secret store component. + /// + /// An optional collection of metadata key-value pairs that will be provided to the secret store component. + /// The valid metadata keys and values are determined by the type of secret store used. + /// + /// A that can be used to cancel the operation. + /// + /// A that resolves to a nested dictionary. The outer key is the secret name; the inner + /// dictionary contains one or more key-value pairs representing the secret data. + /// + public abstract Task>> GetBulkSecretAsync( + string storeName, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + + /// + /// Disposes the resources associated with the object. + /// + /// if called by a call to the method; otherwise . + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementClientBuilder.cs b/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementClientBuilder.cs new file mode 100644 index 000000000..99c8320e1 --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementClientBuilder.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Microsoft.Extensions.Configuration; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.SecretsManagement; + +/// +/// Builds a instance with the specified configuration. +/// +/// An optional instance used to resolve default settings. +public sealed class DaprSecretsManagementClientBuilder(IConfiguration? configuration = null) + : DaprGenericClientBuilder(configuration) +{ + /// + /// Builds the instance from the properties configured on this builder. + /// + /// A new instance. + public override DaprSecretsManagementClient Build() + { + var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprSecretsManagementClient).Assembly); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + return new DaprSecretsManagementGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); + } +} diff --git a/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementGrpcClient.cs b/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementGrpcClient.cs new file mode 100644 index 000000000..ad918418f --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/DaprSecretsManagementGrpcClient.cs @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.SecretsManagement; + +/// +/// A gRPC-backed implementation of that communicates with the +/// Dapr sidecar to retrieve secrets from configured secret store components. +/// +internal sealed class DaprSecretsManagementGrpcClient( + Autogenerated.Dapr.DaprClient client, + HttpClient httpClient, + string? daprApiToken = null) + : DaprSecretsManagementClient(client, httpClient, daprApiToken: daprApiToken) +{ + /// + public override async Task> GetSecretAsync( + string storeName, + string key, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(storeName); + ArgumentException.ThrowIfNullOrEmpty(key); + + var envelope = new Autogenerated.GetSecretRequest + { + StoreName = storeName, + Key = key + }; + + if (metadata is not null) + { + foreach (var kvp in metadata) + { + envelope.Metadata.Add(kvp.Key, kvp.Value); + } + } + + var callOptions = DaprClientUtilities.ConfigureGrpcCallOptions( + typeof(DaprSecretsManagementClient).Assembly, + this.DaprApiToken, + cancellationToken); + + try + { + var response = await Client.GetSecretAsync(envelope, callOptions).ConfigureAwait(false); + return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + catch (Exception ex) + { + throw new DaprException( + "Get secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + public override async Task>> GetBulkSecretAsync( + string storeName, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(storeName); + + var envelope = new Autogenerated.GetBulkSecretRequest + { + StoreName = storeName + }; + + if (metadata is not null) + { + foreach (var kvp in metadata) + { + envelope.Metadata.Add(kvp.Key, kvp.Value); + } + } + + var callOptions = DaprClientUtilities.ConfigureGrpcCallOptions( + typeof(DaprSecretsManagementClient).Assembly, + this.DaprApiToken, + cancellationToken); + + try + { + var response = await Client.GetBulkSecretAsync(envelope, callOptions).ConfigureAwait(false); + return response.Data.ToDictionary( + r => r.Key, + r => (IReadOnlyDictionary)r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(cancellationToken); + } + catch (Exception ex) + { + throw new DaprException( + "Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.HttpClient.Dispose(); + } + } +} diff --git a/src/Dapr.SecretsManagement.Runtime/Extensions/DaprSecretsManagementBuilder.cs b/src/Dapr.SecretsManagement.Runtime/Extensions/DaprSecretsManagementBuilder.cs new file mode 100644 index 000000000..8719856ea --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/Extensions/DaprSecretsManagementBuilder.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.SecretsManagement.Extensions; + +/// +/// Used by the fluent registration builder to configure a Dapr Secrets Management client. +/// +/// The service collection to register services with. +public sealed class DaprSecretsManagementBuilder(IServiceCollection services) : IDaprSecretsManagementBuilder +{ + /// + /// Gets the registered services on the builder. + /// + public IServiceCollection Services { get; } = services; +} diff --git a/src/Dapr.SecretsManagement.Runtime/Extensions/DaprSecretsManagementServiceCollectionExtensions.cs b/src/Dapr.SecretsManagement.Runtime/Extensions/DaprSecretsManagementServiceCollectionExtensions.cs new file mode 100644 index 000000000..bc5b50258 --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/Extensions/DaprSecretsManagementServiceCollectionExtensions.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.SecretsManagement.Extensions; + +/// +/// Contains extension methods for using Dapr Secrets Management with dependency injection. +/// +public static class DaprSecretsManagementServiceCollectionExtensions +{ + /// + /// Adds Dapr Secrets Management client support to the service collection. + /// + /// The to add services to. + /// + /// An optional callback used to configure the with injected services. + /// + /// The lifetime of the registered services. Defaults to . + /// An that can be used for further configuration. + public static IDaprSecretsManagementBuilder AddDaprSecretsManagementClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => + services.AddDaprClient( + configure, lifetime); +} diff --git a/src/Dapr.SecretsManagement.Runtime/IDaprSecretsManagementBuilder.cs b/src/Dapr.SecretsManagement.Runtime/IDaprSecretsManagementBuilder.cs new file mode 100644 index 000000000..e05c81eea --- /dev/null +++ b/src/Dapr.SecretsManagement.Runtime/IDaprSecretsManagementBuilder.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.SecretsManagement; + +/// +/// Responsible for registering Dapr Secrets Management service functionality. +/// +public interface IDaprSecretsManagementBuilder : IDaprServiceBuilder; diff --git a/src/Dapr.SecretsManagement/Dapr.SecretsManagement.csproj b/src/Dapr.SecretsManagement/Dapr.SecretsManagement.csproj new file mode 100644 index 000000000..6bfabacb1 --- /dev/null +++ b/src/Dapr.SecretsManagement/Dapr.SecretsManagement.csproj @@ -0,0 +1,162 @@ + + + + true + Dapr.SecretsManagement + Dapr Secrets Management SDK for .NET + + + true + + + false + + + true + + + $(NoWarn);NU5128 + + + false + + + + $(TargetsForTfmSpecificContentInPackage); + _BuildChildrenForCurrentTFM; + _CollectChildOutputsForCurrentTFM; + _AddAnalyzerOnce + + + + $(TargetsForPack);_BuildGeneratorOnce;_AddAnalyzer + + + + + + <_ExpectedDirectoryBuildProps>$(MSBuildProjectDirectory)\..\Directory.Build.props + + + + + + + + + + + + + + + + + + + + + + + + + + <_EffectiveTfmFolder>$(NuGetTargetFrameworkFolderName) + <_EffectiveTfmFolder Condition="'$(_EffectiveTfmFolder)'==''">$(TargetFramework) + + + + + + + + <_ChildDll Include="@(_BuiltChildOutputs)" Condition="'%(_BuiltChildOutputs.Extension)'=='.dll'" /> + + + <_ChildXml Include="%(_ChildDll.RootDir)%(_ChildDll.Directory)%(_ChildDll.Filename).xml" + Condition="Exists('%(_ChildDll.RootDir)%(_ChildDll.Directory)%(_ChildDll.Filename).xml')" /> + <_ChildPdb Include="%(_ChildDll.RootDir)%(_ChildDll.Directory)%(_ChildDll.Filename).pdb" + Condition="Exists('%(_ChildDll.RootDir)%(_ChildDll.Directory)%(_ChildDll.Filename).pdb')" /> + + + + $(TargetFramework) + lib/$(_EffectiveTfmFolder)/%(_ChildDll.Filename)%(_ChildDll.Extension) + + + + $(TargetFramework) + lib/$(_EffectiveTfmFolder)/%(_ChildXml.Filename).xml + + + + $(TargetFramework) + lib/$(_EffectiveTfmFolder)/%(_ChildPdb.Filename).pdb + + + + + + + + + <_IsFirstTfm Condition="'$(TargetFramework)' == 'net8.0'">true + + + + + + + + + + + + + + + + + <_AddAnalyzer Condition="'$(TargetFramework)' == 'net8.0'">true + + + + + $(TargetFramework) + analyzers/dotnet/cs/%(_AnalyzerOut.Filename)%(_AnalyzerOut.Extension) + + + + + + + + + + + + + + + + diff --git a/test/Dapr.SecretsManagement.Runtime.Test/Dapr.SecretsManagement.Runtime.Test.csproj b/test/Dapr.SecretsManagement.Runtime.Test/Dapr.SecretsManagement.Runtime.Test.csproj new file mode 100644 index 000000000..4561e68b0 --- /dev/null +++ b/test/Dapr.SecretsManagement.Runtime.Test/Dapr.SecretsManagement.Runtime.Test.csproj @@ -0,0 +1,29 @@ + + + + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementClientBuilderTests.cs b/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementClientBuilderTests.cs new file mode 100644 index 000000000..b32e1e998 --- /dev/null +++ b/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementClientBuilderTests.cs @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using Grpc.Net.Client; + +namespace Dapr.SecretsManagement.Test; + +public sealed class DaprSecretsManagementClientBuilderTests +{ + [Fact] + public void Builder_UsesPropertyNameCaseInsensitiveByDefault() + { + var builder = new DaprSecretsManagementClientBuilder(); + Assert.True(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void Builder_UsesPropertyNameCaseHandlingAsSpecified() + { + var builder = new DaprSecretsManagementClientBuilder(); + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = false + }); + Assert.False(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void Builder_UsesThrowOperationCanceledOnCancellation_ByDefault() + { + var builder = new DaprSecretsManagementClientBuilder(); + _ = builder.Build(); + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void Builder_DoesNotOverrideUserGrpcChannelOptions() + { + var builder = new DaprSecretsManagementClientBuilder(); + _ = builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void Builder_ValidatesGrpcEndpointScheme() + { + var builder = new DaprSecretsManagementClientBuilder(); + builder.UseGrpcEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The gRPC endpoint must use http or https.", ex.Message); + } + + [Fact] + public void Builder_ValidatesHttpEndpointScheme() + { + var builder = new DaprSecretsManagementClientBuilder(); + builder.UseHttpEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The HTTP endpoint must use http or https.", ex.Message); + } + + [Fact] + public void Builder_SetsApiToken() + { + var builder = new DaprSecretsManagementClientBuilder(); + builder.UseDaprApiToken("test_token"); + _ = builder.Build(); + Assert.Equal("test_token", builder.DaprApiToken); + } + + [Fact] + public void Builder_SetsNullApiToken() + { + var builder = new DaprSecretsManagementClientBuilder(); + builder.UseDaprApiToken(null!); + _ = builder.Build(); + Assert.Null(builder.DaprApiToken); + } + + [Fact] + public void Builder_SetsTimeout() + { + var builder = new DaprSecretsManagementClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + _ = builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } +} diff --git a/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementGrpcClientTests.cs b/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementGrpcClientTests.cs new file mode 100644 index 000000000..1856c27fc --- /dev/null +++ b/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementGrpcClientTests.cs @@ -0,0 +1,100 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Moq; + +namespace Dapr.SecretsManagement.Test; + +public sealed class DaprSecretsManagementGrpcClientTests +{ + [Fact] + public async Task GetSecretAsync_ThrowsOnNullStoreName() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprSecretsManagementGrpcClient(mockClient, httpClient, null); + + await Assert.ThrowsAsync(async () => + { + await client.GetSecretAsync(null!, "my-key", cancellationToken: TestContext.Current.CancellationToken); + }); + } + + [Fact] + public async Task GetSecretAsync_ThrowsOnNullKey() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprSecretsManagementGrpcClient(mockClient, httpClient, null); + + await Assert.ThrowsAsync(async () => + { + await client.GetSecretAsync("my-store", null!, cancellationToken: TestContext.Current.CancellationToken); + }); + } + + [Fact] + public async Task GetSecretAsync_ThrowsOnEmptyKey() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprSecretsManagementGrpcClient(mockClient, httpClient, null); + + await Assert.ThrowsAsync(async () => + { + await client.GetSecretAsync("my-store", "", cancellationToken: TestContext.Current.CancellationToken); + }); + } + + [Fact] + public async Task GetBulkSecretAsync_ThrowsOnNullStoreName() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprSecretsManagementGrpcClient(mockClient, httpClient, null); + + await Assert.ThrowsAsync(async () => + { + await client.GetBulkSecretAsync(null!, cancellationToken: TestContext.Current.CancellationToken); + }); + } + + [Fact] + public void Dispose_CanBeCalledWithoutException() + { + var mockClient = Mock.Of(); + var httpClient = new HttpClient(); + + var client = new DaprSecretsManagementGrpcClient(mockClient, httpClient, null); + + // Should not throw. + client.Dispose(); + } + + [Fact] + public void Dispose_IsIdempotent() + { + var mockClient = Mock.Of(); + var httpClient = new HttpClient(); + + var client = new DaprSecretsManagementGrpcClient(mockClient, httpClient, null); + + // Calling Dispose multiple times should not throw. + client.Dispose(); + client.Dispose(); + } +} diff --git a/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementServiceCollectionExtensionsTests.cs b/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..1aff866d3 --- /dev/null +++ b/test/Dapr.SecretsManagement.Runtime.Test/DaprSecretsManagementServiceCollectionExtensionsTests.cs @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.SecretsManagement.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.SecretsManagement.Test; + +public sealed class DaprSecretsManagementServiceCollectionExtensionsTests +{ + [Fact] + public void AddDaprSecretsManagementClient_RegistersClientInServiceCollection() + { + var services = new ServiceCollection(); + var builder = services.AddDaprSecretsManagementClient(); + + Assert.NotNull(builder); + Assert.IsAssignableFrom(builder); + + var descriptor = services.FirstOrDefault(sd => sd.ServiceType == typeof(DaprSecretsManagementClient)); + Assert.NotNull(descriptor); + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + } + + [Fact] + public void AddDaprSecretsManagementClient_RespectsCustomLifetime() + { + var services = new ServiceCollection(); + services.AddDaprSecretsManagementClient(lifetime: ServiceLifetime.Transient); + + var descriptor = services.FirstOrDefault(sd => sd.ServiceType == typeof(DaprSecretsManagementClient)); + Assert.NotNull(descriptor); + Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime); + } + + [Fact] + public void AddDaprSecretsManagementClient_BuilderExposesSameServices() + { + var services = new ServiceCollection(); + var builder = services.AddDaprSecretsManagementClient(); + + Assert.Same(services, builder.Services); + } +}