├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build-and-publish.yml │ ├── build-and-test.yml │ └── snyk-test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── Examples ├── Example.Secrets.Aws.AspNetCore │ ├── City.cs │ ├── Example.Secrets.Aws.AspNetCore.csproj │ ├── Program.cs │ ├── README.md │ ├── Startup.cs │ └── appsettings.json └── Example.Secrets.Custom │ ├── Example.Secrets.Custom.csproj │ ├── Program.cs │ ├── ReversedSecret.cs │ └── appsettings.json ├── LICENSE.md ├── README.md ├── RockLib.Secrets.All.sln ├── RockLib.Secrets.Aws ├── AwsSecret.cs ├── AwsSecretsConfigurationBuilderExtensions.cs ├── CHANGELOG.md ├── RockLib.Secrets.Aws.csproj └── RockLib.Secrets.Aws.sln ├── RockLib.Secrets ├── AssemblyAttributes.cs ├── CHANGELOG.md ├── ConfigurationBuilderExtensions.cs ├── ISecret.cs ├── ISecretsConfigurationBuilder.cs ├── RockLib.Secrets.csproj ├── RockLib.Secrets.sln ├── SecretExceptionContext.cs ├── SecretsConfigurationBuilder.cs ├── SecretsConfigurationProvider.cs └── SecretsConfigurationSource.cs ├── Tests ├── RockLib.Secrets.Aws.Tests │ ├── AssemblySettings.cs │ ├── AwsSecretTests.cs │ ├── AwsSecretsConfigurationBuilderExtensionsTests .cs │ ├── FakeSecretsManager.cs │ ├── RockLib.Secrets.Aws.Tests.csproj │ └── appsettings.json └── RockLib.Secrets.Tests │ ├── AssemblySettings.cs │ ├── ConfigurationBuilderExtensionsTests.cs │ ├── MockSecret.cs │ ├── RockLib.Secrets.Tests.csproj │ ├── SecretsConfigurationBuilderTests.cs │ ├── SecretsConfigurationProviderTests.cs │ └── SecretsConfigurationSourceTests.cs ├── docs ├── AwsSecret.md ├── Configuration.md ├── ConfigurationBuilder.md ├── GettingStarted.md └── Usage.md └── icon.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | 6 | [*.cs] 7 | # Styling 8 | indent_style = space 9 | indent_size = 4 10 | csharp_indent_case_contents = true 11 | csharp_indent_switch_labels = true 12 | csharp_new_line_before_catch = true 13 | csharp_new_line_before_else = true 14 | csharp_new_line_before_finally = true 15 | csharp_new_line_before_members_in_anonymous_types = false 16 | csharp_new_line_before_members_in_object_initializers = false 17 | csharp_new_line_before_open_brace = methods, control_blocks, types, properties, lambdas, accessors, object_collection_array_initializers 18 | csharp_new_line_between_query_expression_clauses = true 19 | csharp_prefer_braces = false:suggestion 20 | csharp_prefer_simple_default_expression = true:suggestion 21 | csharp_preferred_modifier_order = public,private,internal,protected,static,readonly,async,override,sealed:suggestion 22 | csharp_preserve_single_line_blocks = true 23 | csharp_preserve_single_line_statements = true 24 | csharp_space_after_cast = false 25 | csharp_space_after_colon_in_inheritance_clause = true 26 | csharp_space_after_keywords_in_control_flow_statements = true 27 | csharp_space_before_colon_in_inheritance_clause = true 28 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 29 | csharp_space_between_method_call_name_and_opening_parenthesis = false 30 | csharp_space_between_method_call_parameter_list_parentheses = false 31 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 32 | csharp_space_between_method_declaration_parameter_list_parentheses = false 33 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 34 | csharp_style_expression_bodied_accessors = true:suggestion 35 | csharp_style_expression_bodied_constructors = false:suggestion 36 | csharp_style_expression_bodied_methods = false:suggestion 37 | csharp_style_expression_bodied_properties = true:suggestion 38 | csharp_style_inlined_variable_declaration = true:suggestion 39 | csharp_style_var_elsewhere = true:suggestion 40 | csharp_style_var_for_built_in_types = true:suggestion 41 | csharp_style_var_when_type_is_apparent = true:suggestion 42 | dotnet_sort_system_directives_first = false 43 | dotnet_style_explicit_tuple_names = true:suggestion 44 | dotnet_style_object_initializer = true:suggestion 45 | csharp_style_pattern_local_over_anonymous_function = false:suggestion 46 | dotnet_style_predefined_type_for_member_access = true:suggestion 47 | dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion 48 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 49 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 50 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 51 | dotnet_style_qualification_for_field = false:suggestion 52 | dotnet_style_qualification_for_method = false:suggestion 53 | dotnet_style_qualification_for_property = false:suggestion 54 | 55 | 56 | # Analyzer Configuration 57 | # These are rules we want to either ignore or have set as suggestion or info 58 | 59 | # CA1014: Mark assemblies with CLSCompliant 60 | # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1014 61 | dotnet_diagnostic.CA1014.severity = none 62 | 63 | # CA1725: Parameter names should match base declaration 64 | # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1725 65 | dotnet_diagnostic.CA1725.severity = suggestion 66 | 67 | # CA2227: Collection properties should be read only 68 | # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2227 69 | dotnet_diagnostic.CA2227.severity = suggestion 70 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 12 | 13 | ## Type of change: 14 | 15 | 1. Non-functional change (e.g. documentation changes, removing unused `using` directives, renaming local variables, etc) 16 | 2. Bug fix (non-breaking change that fixes an issue) 17 | 3. New feature (non-breaking change that adds functionality) 18 | 4. Breaking change (fix or feature that could cause existing functionality to not work as expected) 19 | 20 | ## Checklist: 21 | 22 | - Have you reviewed your own code? Do you understand every change? 23 | - Are you following the [contributing guidelines](../blob/main/CONTRIBUTING.md)? 24 | - Have you added tests that prove your fix is effective or that this feature works? 25 | - New and existing unit tests pass locally with these changes? 26 | - Have you made corresponding changes to the documentation? 27 | - Will this change require an update to an example project? (if so, create an issue and link to it) 28 | 29 | --- 30 | 31 | _[Reviewer guidelines](../blob/main/CONTRIBUTING.md#reviewing-changes)_ 32 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | #################################################################################################### 4 | ## WORKFLOW TRIGGER 5 | #################################################################################################### 6 | on: 7 | # Workflow will run when a release is published. 8 | release: 9 | types: [ released, prereleased ] 10 | 11 | #################################################################################################### 12 | ## WORKFLOW JOBS 13 | #################################################################################################### 14 | jobs: 15 | # Calls the shared build-and-publish workflow. 16 | call_build_and_publish: 17 | name: Call build-and-publish workflow 18 | uses: RockLib/RockLib.Workflows/.github/workflows/build-and-publish.yml@main 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Test 2 | 3 | #################################################################################################### 4 | ## WORKFLOW TRIGGER 5 | #################################################################################################### 6 | on: 7 | # Workflow will run on pull requests to the main branch. 8 | pull_request: 9 | branches: [ main ] 10 | 11 | #################################################################################################### 12 | ## WORKFLOW JOBS 13 | #################################################################################################### 14 | jobs: 15 | # Calls the shared unit-test workflow. 16 | call_unit_test: 17 | name: Call unit-test workflow 18 | uses: RockLib/RockLib.Workflows/.github/workflows/unit-test.yml@main 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/snyk-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Snyk Test 2 | 3 | #################################################################################################### 4 | ## WORKFLOW TRIGGER 5 | #################################################################################################### 6 | on: 7 | # Workflow will run after unit-test is completed. 8 | workflow_run: 9 | workflows: [ Run Unit Test ] 10 | types: [ completed ] 11 | 12 | #################################################################################################### 13 | ## WORKFLOW JOBS 14 | #################################################################################################### 15 | jobs: 16 | # Calls the shared snyk-test workflow. 17 | call_snyk_test: 18 | name: Call snyk-test workflow 19 | uses: RockLib/RockLib.Workflows/.github/workflows/snyk-test.yml@main 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at RockLibSupport@quickenloans.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to RockLib 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | Following these guidelines helps to communicate that you respect the time of 7 | the developers managing and developing this open source project. In return, 8 | they should reciprocate that respect in addressing your issue, assessing 9 | changes, and helping you finalize your pull requests. 10 | 11 | As for everything else in the project, the contributions to RockLib are governed by our [Code of Conduct](CODE_OF_CONDUCT.md). 12 | 13 | 14 | ## Using the issue tracker 15 | 16 | First things first: **Do NOT report security vulnerabilities in public issues!** Please disclose responsibly by letting [the RockLib team](mailto:RockLibSupport@quickenloans.com?subject=Security) know upfront. We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix and release available for an eventual public disclosure. 17 | 18 | The GitHub issue tracker is the preferred channel for [bug reports](#bugs), 19 | [features requests](#features) and [submitting pull 20 | requests](#pull-requests). 21 | 22 | 23 | ## Bug reports 24 | 25 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 26 | Good bug reports are extremely helpful - thank you! 27 | 28 | Guidelines for bug reports: 29 | 30 | 1. **Use the GitHub issue search** — check if the issue has already been 31 | reported. 32 | 33 | 2. **Check if the issue has been fixed** — try to reproduce it using the 34 | latest `master` branch in the repository. 35 | 36 | 3. **Isolate the problem** — ideally create a reduced test case. 37 | 38 | A good bug report shouldn't leave others needing to chase you up for more 39 | information. Please try to be as detailed as possible in your report. What is 40 | your environment? What steps will reproduce the issue? What OS experiences the 41 | problem? What would you expect to be the outcome? All these details will help 42 | people to fix any potential bugs. 43 | 44 | Example: 45 | 46 | > Short and descriptive example bug report title 47 | > 48 | > A summary of the issue and the browser/OS environment in which it occurs. If 49 | > suitable, include the steps required to reproduce the bug. 50 | > 51 | > 1. This is the first step 52 | > 2. This is the second step 53 | > 3. Further steps, etc. 54 | > 55 | > `` - a link to the reduced test case 56 | > 57 | > Any other information you want to share that is relevant to the issue being 58 | > reported. This might include the lines of code that you have identified as 59 | > causing the bug, and potential solutions (and your opinions on their 60 | > merits). 61 | 62 | 63 | ## Feature requests 64 | 65 | Feature requests are welcome. But take a moment to find out whether your idea 66 | fits with the scope and aims of the project. It's up to *you* to make a strong 67 | case to convince the project's developers of the merits of this feature. Please 68 | provide as much detail and context as possible. 69 | 70 | 71 | ## Pull requests 72 | 73 | Good pull requests - patches, improvements, new features - are a fantastic 74 | help. They should remain focused in scope and avoid containing unrelated 75 | commits. 76 | 77 | **Please ask first** before embarking on any significant pull request (e.g. 78 | implementing features, refactoring code), otherwise you risk spending a lot of 79 | time working on something that the project's developers might not want to merge 80 | into the project. 81 | 82 | 83 | ### For Contributors 84 | 85 | If you have never created a pull request before, welcome :tada: :smile: [Here is a great tutorial](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) 86 | on how to create a pull request.. 87 | 88 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 89 | and configure the remotes: 90 | 91 | ```bash 92 | # Clone your fork of the repo into the current directory 93 | git clone https://github.com// 94 | # Navigate to the newly cloned directory 95 | cd 96 | # Assign the original repo to a remote called "upstream" 97 | git remote add upstream https://github.com/RockLib/ 98 | ``` 99 | 100 | 2. If you cloned a while ago, get the latest changes from upstream: 101 | 102 | ```bash 103 | git checkout master 104 | git pull upstream master 105 | ``` 106 | 107 | 3. Create a new topic branch (off the main project development branch) to 108 | contain your feature, change, or fix: 109 | 110 | ```bash 111 | git checkout -b 112 | ``` 113 | 114 | 4. Please follow Chris Beams' [seven rules of a great Git commit message](https://chris.beams.io/posts/git-commit/#seven-rules): 115 | 116 | 1. [Separate subject from body with a blank line](https://chris.beams.io/posts/git-commit/#separate) 117 | 2. [Limit the subject line to 50 characters](https://chris.beams.io/posts/git-commit/#limit-50) 118 | 3. [Capitalize the subject line](https://chris.beams.io/posts/git-commit/#capitalize) 119 | 4. [Do not end the subject line with a period](https://chris.beams.io/posts/git-commit/#end) 120 | 5. [Use the imperative mood in the subject line](https://chris.beams.io/posts/git-commit/#imperative) 121 | 6. [Wrap the body at 72 characters](https://chris.beams.io/posts/git-commit/#wrap-72) 122 | 7. [Use the body to explain what and why vs. how](https://chris.beams.io/posts/git-commit/#why-not-how) 123 | 124 | 5. Make sure to update, or add to the tests when appropriate. 125 | 126 | 6. If you added or changed a feature, make sure to document it accordingly in 127 | the `README.md` file. 128 | 129 | 7. Push your topic branch up to your fork: 130 | 131 | ```bash 132 | git push origin 133 | ``` 134 | 135 | 8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 136 | with a clear title and description. 137 | 138 | 139 | **IMPORTANT**: By submitting a patch, you agree to license your work under the 140 | same license as that used by the project. 141 | 142 | ## Maintainers 143 | 144 | If you have commit access, please follow this process for merging patches and cutting new releases. 145 | 146 | ### Reviewing changes 147 | 148 | 1. Check that a change is within the scope and philosophy of the component. 149 | 2. Check that a change has any necessary tests. 150 | 3. Check that a change has any necessary documentation. 151 | 4. If there is anything you don’t like, leave a comment below the respective 152 | lines and submit a "Request changes" review. Repeat until everything has 153 | been addressed. 154 | 5. If you are not sure about something, mention specific people for help in a 155 | comment. 156 | 6. If there is only a tiny change left before you can merge it and you think 157 | it’s best to fix it yourself, do so and leave a comment about it so the 158 | author and others will know. 159 | 7. Once everything looks good, add an "Approve" review. Don’t forget to say 160 | something nice 👏🐶💖✨ 161 | 8. If the commit messages follow Chris Beams' [seven rules of a great Git commit 162 | message](https://chris.beams.io/posts/git-commit/#seven-rules): 163 | 164 | 1. Use the "Merge pull request" button to merge the pull request. 165 | 2. Done! You are awesome! Thanks so much for your help 🤗 166 | 167 | 9. If the commit messages _do not_ follow our conventions: 168 | 169 | 1. Use the "Squash and merge" button to clean up the commits and merge at 170 | the same time: ✨🎩 171 | 2. Add a new commit subject and body. 172 | 173 | --- 174 | 175 | *This document is based on the [contributing](https://github.com/hoodiehq/hoodie/blob/master/CONTRIBUTING.md) document from the [Hoodie](https://github.com/hoodiehq/hoodie) project.* 176 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | all 7 | Rocket Mortgage 8 | Copyright 2025 (c) Rocket Mortgage. All rights reserved. 9 | latest 10 | enable 11 | net48;net8.0 12 | NU1603,NU1701 13 | true 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | -------------------------------------------------------------------------------- /Examples/Example.Secrets.Aws.AspNetCore/City.cs: -------------------------------------------------------------------------------- 1 | namespace Example.Secrets.Aws.AspNetCore 2 | { 3 | public enum City 4 | { 5 | Detroit, 6 | Chicago, 7 | London, 8 | Tokyo, 9 | Auckland 10 | } 11 | } -------------------------------------------------------------------------------- /Examples/Example.Secrets.Aws.AspNetCore/Example.Secrets.Aws.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | enable 4 | CA1812 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/Example.Secrets.Aws.AspNetCore/Program.cs: -------------------------------------------------------------------------------- 1 | using Example.Secrets.Aws.AspNetCore; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Newtonsoft.Json.Linq; 4 | using RockLib.Secrets; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | builder.Configuration.SetSecretExceptionHandler(context => Console.WriteLine(context.Exception)) 8 | .AddRockLibSecrets(); 9 | 10 | builder.Services.AddHttpClient("OpenUV", 11 | client => client.DefaultRequestHeaders.Add("x-access-token", builder.Configuration["OpenUVApiKey"])); 12 | 13 | var application = builder.Build(); 14 | 15 | application.MapGet("/{city}", async (IHttpClientFactory factory, [FromRoute] City city) => 16 | { 17 | GetCoordinates(city, out var latitude, out var longitude); 18 | 19 | using var httpClient = factory.CreateClient("OpenUV"); 20 | var response = await httpClient.GetAsync( 21 | new Uri($"https://api.openuv.io/api/v1/uv?lat={latitude}&lng={longitude}")).ConfigureAwait(false); 22 | var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 23 | 24 | dynamic parsedContent = JObject.Parse(content); 25 | return parsedContent.result.uv; 26 | }); 27 | 28 | if (application.Environment.IsDevelopment()) 29 | { 30 | application.UseDeveloperExceptionPage(); 31 | } 32 | 33 | application.UseHttpsRedirection(); 34 | application.UseRouting(); 35 | application.UseAuthorization(); 36 | #pragma warning disable ASP0014 // Suggest using top level route registrations 37 | application.UseEndpoints(endpoints => 38 | { 39 | endpoints.MapControllers(); 40 | }); 41 | #pragma warning restore ASP0014 // Suggest using top level route registrations 42 | 43 | application.Run(); 44 | 45 | static void GetCoordinates(City city, out double latitude, out double longitude) 46 | { 47 | switch (city) 48 | { 49 | case City.Detroit: 50 | latitude = 42.331389; 51 | longitude = -83.045833; 52 | break; 53 | 54 | case City.Chicago: 55 | latitude = 41.881944; 56 | longitude = -87.627778; 57 | break; 58 | 59 | case City.London: 60 | latitude = 51.507222; 61 | longitude = -0.1275; 62 | break; 63 | 64 | case City.Tokyo: 65 | latitude = 35.689722; 66 | longitude = 139.692222; 67 | break; 68 | 69 | case City.Auckland: 70 | latitude = -36.840556; 71 | longitude = 174.74; 72 | break; 73 | 74 | default: 75 | throw new ArgumentOutOfRangeException(nameof(city), city, null); 76 | } 77 | } -------------------------------------------------------------------------------- /Examples/Example.Secrets.Aws.AspNetCore/README.md: -------------------------------------------------------------------------------- 1 | # Example.Secrets.Aws.AspNetCore 2 | 3 | This example app provides an API with a single endpoint, which returns the UV index of selected cities. The endpoint is `/uv/{city}` and the available cities are Detroit, Chicago, London, Tokyo, and Auckland. For example, `/uv/Tokyo`. 4 | 5 | In order to run this example, there are some prerequisites: 6 | 7 | 1. [An API Key for the free service at OpenUV](#openuv) 8 | 2. [The API Key is stored in an AWS secret](#aws-secret) 9 | 3. The machine running the example app has an AWS profile that has access to the above AWS secret. See [Getting Started with the AWS SDK for .NET 10 | ](https://docs.aws.amazon.com/sdk-for-net/v2/developer-guide/net-dg-setup.html) for details. 11 | 12 | ## OpenUV 13 | 14 | The Open UV API was selected for the Example project because it allowed for the API Key to be passed using an HTTP Request Header, and account/API Key creation is simple and free. 15 | 16 | To obtain an Open UV API key: 17 | 18 | 1. Create an Open UV Index API account. 19 | - Navigate to https://www.openuv.io/. 20 | - Select Sign In - this will prompt the user to Sign in with Google. 21 | 2. Generate an OpenUV API Key. 22 | - Navigate to https://www.openuv.io/console. 23 | - Copy the generated API Key, or generate a new key. 24 | 25 | *NOTE: The API Key header name is `x-access-token`.* 26 | 27 | ## AWS Secret 28 | 29 | The Open UV Api Key needs to be stored as an AWS Secret: 30 | 31 | 1. Log in to the AWS console. 32 | 2. Navigate to Secrets Manager. 33 | 3. Click on the 'Store a new secret' button. 34 | 4. Select 'Other type of secrets'. 35 | 5. Add a secret with a key of 'OpenUVApiKey' and a value of the Open UV API Key above. 36 | 6. Click 'Next'. 37 | 7. Name the secret 'RockLib.Example.Secret'. 38 | 8. Click through the remainder of the wizard until the secret is created. 39 | -------------------------------------------------------------------------------- /Examples/Example.Secrets.Aws.AspNetCore/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | 10 | namespace Example.Secrets.Aws.AspNetCore 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddHttpClient("OpenUV", client => client.DefaultRequestHeaders.Add("x-access-token", Configuration["OpenUVApiKey"])); 25 | services.AddControllers(); 26 | } 27 | 28 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 29 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 30 | { 31 | if (env.IsDevelopment()) 32 | { 33 | app.UseDeveloperExceptionPage(); 34 | } 35 | 36 | app.UseHttpsRedirection(); 37 | 38 | app.UseRouting(); 39 | 40 | app.UseAuthorization(); 41 | 42 | app.UseEndpoints(endpoints => 43 | { 44 | endpoints.MapControllers(); 45 | }); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/Example.Secrets.Aws.AspNetCore/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | 11 | "OpenUVApiKey": "this secret has not been applied", 12 | 13 | "RockLib.Secrets": [ 14 | { 15 | "Type": "RockLib.Secrets.Aws.AwsSecret, RockLib.Secrets.Aws", 16 | "Value": { 17 | "ConfigurationKey": "OpenUVApiKey", 18 | "SecretId": "RockLib.Example.Secret", 19 | "SecretKey": "OpenUVApiKey" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /Examples/Example.Secrets.Custom/Example.Secrets.Custom.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | enable 4 | CA1812 5 | Exe 6 | net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | PreserveNewest 14 | true 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Examples/Example.Secrets.Custom/Program.cs: -------------------------------------------------------------------------------- 1 | using Example.Secrets.Custom; 2 | using Microsoft.Extensions.Configuration; 3 | using RockLib.Secrets; 4 | 5 | // This example defines two secrets to be added to configuration. The 'SomeApiKey' 6 | // secret is added programmatically and the 'MyConnectionString' secret is defined 7 | // in the appsettings.json file under the 'RockLib.Secrets' section. 8 | 9 | var builder = new ConfigurationBuilder(); 10 | 11 | builder.AddJsonFile("appsettings.json"); 12 | builder.AddRockLibSecrets() 13 | .AddSecret(new ReversedSecret("SomeApiKey", "terces dnoces eht si sihT")); 14 | 15 | var configuration = builder.Build(); 16 | 17 | Console.WriteLine("MyConnectionString: " + configuration["MyConnectionString"]); 18 | Console.WriteLine("SomeApiKey: " + configuration["SomeApiKey"]); -------------------------------------------------------------------------------- /Examples/Example.Secrets.Custom/ReversedSecret.cs: -------------------------------------------------------------------------------- 1 | using RockLib.Secrets; 2 | using System.Linq; 3 | 4 | namespace Example.Secrets.Custom 5 | { 6 | internal sealed class ReversedSecret : ISecret 7 | { 8 | private readonly string _secret; 9 | 10 | public ReversedSecret(string key, string secret) 11 | { 12 | ConfigurationKey = key; 13 | _secret = secret; 14 | } 15 | 16 | public string ConfigurationKey { get; } 17 | 18 | public string GetValue() 19 | { 20 | return new string(_secret.ToCharArray().Reverse().ToArray()); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Example.Secrets.Custom/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MyConnectionString": "The secret for 'MyConnectionString' has not been applied", 3 | "SomeApiKey": "The secret for 'SomeApiKey' has not been applied", 4 | 5 | "RockLib.Secrets": [ 6 | { 7 | "Type": "Example.Secrets.Custom.ReversedSecret, Example.Secrets.Custom", 8 | "Value": { 9 | "Key": "MyConnectionString", 10 | "Secret": ";drowssaPym=drowssaP;emanresUym=dI resU;esaBataDym=esabataD;sserddArevreSym=revreS" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018-2021 Rocket Mortgage 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RockLib.Secrets 2 | 3 | *A configuration provider for Microsoft.Extensions.Configuration backed by runtime secrets.* 4 | 5 | ### RockLib.Secrets 6 | 7 | ### RockLib.Secrets.Aws 8 | 9 | --- 10 | 11 | - [Getting started](docs/GettingStarted.md) 12 | - How to: 13 | - [Add secrets directly to a configuration builder](docs/ConfigurationBuilder.md) 14 | - [Define secrets in configuration](docs/Configuration.md) 15 | - [Consume secrets from configuration](docs/Usage.md) 16 | - [Configure AWS secrets](docs/AwsSecret.md) 17 | - API Reference: 18 | - [RockLib.Secrets](https://www.nuget.org/packages/RockLib.Secrets) 19 | - [RockLib.Secrets.Aws](https://www.nuget.org/packages/RockLib.Secrets.Aws) 20 | -------------------------------------------------------------------------------- /RockLib.Secrets.All.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34616.47 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets", "RockLib.Secrets\RockLib.Secrets.csproj", "{6E303E27-044B-420D-AD41-D07959C9E32F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets.Aws", "RockLib.Secrets.Aws\RockLib.Secrets.Aws.csproj", "{C011E4CB-227F-4369-A6A4-E2880B73F0FA}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{E49937C6-C91E-4FA8-97EA-567D6E9571F8}" 11 | ProjectSection(SolutionItems) = preProject 12 | docs\AwsSecret.md = docs\AwsSecret.md 13 | docs\Configuration.md = docs\Configuration.md 14 | docs\ConfigurationBuilder.md = docs\ConfigurationBuilder.md 15 | docs\GettingStarted.md = docs\GettingStarted.md 16 | README.md = README.md 17 | docs\Usage.md = docs\Usage.md 18 | EndProjectSection 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6B7B23BA-A05D-4030-AEF7-6DF4467CAAB8}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{AF5ACD25-1ACA-417C-B08C-6240D79595F2}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Secrets.Aws.AspNetCore", "Examples\Example.Secrets.Aws.AspNetCore\Example.Secrets.Aws.AspNetCore.csproj", "{6159D1AD-791A-4835-8D6B-4A35CB147280}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Secrets.Custom", "Examples\Example.Secrets.Custom\Example.Secrets.Custom.csproj", "{BC010E7A-712E-40E1-95AF-E13AB95FDEF9}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets.Aws.Tests", "Tests\RockLib.Secrets.Aws.Tests\RockLib.Secrets.Aws.Tests.csproj", "{30887B96-1ABA-44BE-8A9B-842429E01B4B}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets.Tests", "Tests\RockLib.Secrets.Tests\RockLib.Secrets.Tests.csproj", "{453034CF-0EEF-4244-9558-49686D0F605E}" 31 | EndProject 32 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{21241C4A-E465-4A29-99A2-4E7EEEF5482B}" 33 | ProjectSection(SolutionItems) = preProject 34 | .editorconfig = .editorconfig 35 | CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md 36 | CONTRIBUTING.md = CONTRIBUTING.md 37 | Directory.Build.props = Directory.Build.props 38 | LICENSE.md = LICENSE.md 39 | README.md = README.md 40 | EndProjectSection 41 | EndProject 42 | Global 43 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 44 | Debug|Any CPU = Debug|Any CPU 45 | Release|Any CPU = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 48 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {6159D1AD-791A-4835-8D6B-4A35CB147280}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {6159D1AD-791A-4835-8D6B-4A35CB147280}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {6159D1AD-791A-4835-8D6B-4A35CB147280}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {6159D1AD-791A-4835-8D6B-4A35CB147280}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {BC010E7A-712E-40E1-95AF-E13AB95FDEF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {BC010E7A-712E-40E1-95AF-E13AB95FDEF9}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {BC010E7A-712E-40E1-95AF-E13AB95FDEF9}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {BC010E7A-712E-40E1-95AF-E13AB95FDEF9}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {30887B96-1ABA-44BE-8A9B-842429E01B4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {30887B96-1ABA-44BE-8A9B-842429E01B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {30887B96-1ABA-44BE-8A9B-842429E01B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {30887B96-1ABA-44BE-8A9B-842429E01B4B}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {453034CF-0EEF-4244-9558-49686D0F605E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {453034CF-0EEF-4244-9558-49686D0F605E}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {453034CF-0EEF-4244-9558-49686D0F605E}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {453034CF-0EEF-4244-9558-49686D0F605E}.Release|Any CPU.Build.0 = Release|Any CPU 72 | EndGlobalSection 73 | GlobalSection(SolutionProperties) = preSolution 74 | HideSolutionNode = FALSE 75 | EndGlobalSection 76 | GlobalSection(NestedProjects) = preSolution 77 | {6159D1AD-791A-4835-8D6B-4A35CB147280} = {AF5ACD25-1ACA-417C-B08C-6240D79595F2} 78 | {BC010E7A-712E-40E1-95AF-E13AB95FDEF9} = {AF5ACD25-1ACA-417C-B08C-6240D79595F2} 79 | {30887B96-1ABA-44BE-8A9B-842429E01B4B} = {6B7B23BA-A05D-4030-AEF7-6DF4467CAAB8} 80 | {453034CF-0EEF-4244-9558-49686D0F605E} = {6B7B23BA-A05D-4030-AEF7-6DF4467CAAB8} 81 | EndGlobalSection 82 | GlobalSection(ExtensibilityGlobals) = postSolution 83 | SolutionGuid = {A43A5F42-B927-48DA-9BC9-D6123CFE9BDC} 84 | EndGlobalSection 85 | EndGlobal 86 | -------------------------------------------------------------------------------- /RockLib.Secrets.Aws/AwsSecret.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using Amazon.SecretsManager; 5 | using Amazon.SecretsManager.Model; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace RockLib.Secrets.Aws 9 | { 10 | /// 11 | /// An implementation of the interface for use with AWS. 12 | /// 13 | public class AwsSecret : ISecret 14 | { 15 | private readonly string _exceptionMessageFormat; 16 | 17 | /// 18 | /// Initializes a new instance of the class for key/value secrets. 19 | /// 20 | /// The configuration key for the secret. 21 | /// The Amazon Resource Name (ARN) or the friendly name of the secret. 22 | /// The key of the secret in AWS. 23 | /// 24 | /// The client used for routing calls to AWS. If null, an instance of will be used. 25 | /// 26 | public AwsSecret(string configurationKey, string secretId, string? secretKey = null, 27 | IAmazonSecretsManager? secretsManager = null) 28 | { 29 | ConfigurationKey = configurationKey ?? throw new ArgumentNullException(nameof(configurationKey)); 30 | SecretId = secretId ?? throw new ArgumentNullException(nameof(secretId)); 31 | SecretKey = secretKey; 32 | SecretsManager = secretsManager; 33 | 34 | if (secretKey is null) 35 | { 36 | _exceptionMessageFormat = $"No secret was found with the AwsSecretName '{SecretId}' for the key '{ConfigurationKey}': {{0}}"; 37 | } 38 | else 39 | { 40 | _exceptionMessageFormat = $"No secret was found with the AwsSecretName '{SecretId}' and AwsSecretKey '{SecretKey}' for the key '{ConfigurationKey}': {{0}}"; 41 | } 42 | } 43 | /// 44 | /// Gets the configuration key for the secret. 45 | /// 46 | public string ConfigurationKey { get; } 47 | 48 | /// 49 | /// Gets the Amazon Resource Name (ARN) or the friendly name of the secret. 50 | /// 51 | public string SecretId { get; } 52 | 53 | /// 54 | /// Gets the "Secret Key" of the secret. 55 | /// 56 | public string? SecretKey { get; } 57 | 58 | /// 59 | /// Gets the client used for routing calls to AWS. 60 | /// 61 | public IAmazonSecretsManager? SecretsManager { get; private set; } 62 | 63 | /// 64 | /// Gets the value of the secret. 65 | /// 66 | /// The secret value. 67 | public string GetValue() 68 | { 69 | var request = new GetSecretValueRequest 70 | { 71 | SecretId = SecretId 72 | }; 73 | 74 | // Set the manager to the AWS one if it wasn't provided. 75 | SecretsManager ??= new AmazonSecretsManagerClient(); 76 | 77 | // NOTE: This is NOT ideal. We should be awaiting the call. 78 | // But ISecret only has a sync version for GetValue(). 79 | // Furthermore, providing an async version (GetValueAsync()) doesn't help, 80 | // because configuration is synchronous. 81 | // This issue, if resolved, would fix this: https://github.com/dotnet/runtime/issues/36018 82 | // But it's still open :(. So we have to block the call here for now. 83 | var response = SecretsManager.GetSecretValueAsync(request).GetAwaiter().GetResult(); 84 | 85 | if (response is null) 86 | { 87 | throw new KeyNotFoundException( 88 | string.Format(CultureInfo.InvariantCulture, 89 | _exceptionMessageFormat, "Response was null.")); 90 | } 91 | 92 | if (response.SecretString is not null) 93 | { 94 | if (SecretKey is not null) 95 | { 96 | var secret = JObject.Parse(response.SecretString)[SecretKey]; 97 | 98 | if (secret is not null) 99 | { 100 | return secret.ToString(); 101 | } 102 | 103 | throw new KeyNotFoundException( 104 | string.Format(CultureInfo.InvariantCulture, 105 | _exceptionMessageFormat, $"Response did not contain item with the name '{SecretKey}'.")); 106 | } 107 | 108 | return response.SecretString; 109 | } 110 | 111 | if (response.SecretBinary is not null) 112 | { 113 | return Convert.ToBase64String(response.SecretBinary.ToArray()); 114 | } 115 | 116 | throw new KeyNotFoundException( 117 | string.Format(CultureInfo.InvariantCulture, 118 | _exceptionMessageFormat, "Response did not contain a value for SecretString or SecretBinary.")); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /RockLib.Secrets.Aws/AwsSecretsConfigurationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Amazon.SecretsManager; 2 | using System; 3 | 4 | namespace RockLib.Secrets.Aws 5 | { 6 | /// 7 | /// Extension methods for secrets configuration builders related to AWS. 8 | /// 9 | public static class AwsSecretsConfigurationBuilderExtensions 10 | { 11 | /// 12 | /// Adds an to the . 13 | /// 14 | /// The builder the secret is beind added to. 15 | /// The configuration key for the secret. 16 | /// The Amazon Resource Name (ARN) or the friendly name of the secret. 17 | /// The key of the secret in AWS. 18 | /// 19 | /// The client used for routing calls to AWS. 20 | /// 21 | /// The same . 22 | public static ISecretsConfigurationBuilder AddAwsSecret(this ISecretsConfigurationBuilder builder, 23 | string configurationKey, string secretId, string? secretKey, IAmazonSecretsManager secretsManager) 24 | { 25 | #if NET6_0_OR_GREATER 26 | ArgumentNullException.ThrowIfNull(builder); 27 | #else 28 | if (builder is null) 29 | { 30 | throw new ArgumentNullException(nameof(builder)); 31 | } 32 | #endif 33 | 34 | return builder.AddSecret(new AwsSecret(configurationKey, secretId, secretKey, secretsManager)); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /RockLib.Secrets.Aws/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # RockLib.Secrets.Aws Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 5.0.0 - 2025-02-05 9 | 10 | #### Changed 11 | - Finalized 5.0.0 release 12 | - RockLib.Secrets.6.0.0-alpha.1 -> RockLib.Secrets.6.0.0 13 | 14 | ## 5.0.0-alpha.1 - 2025-02-05 15 | 16 | #### Changed 17 | - RockLib.Secrets.5.0.0 -> RockLib.Secrets.6.0.0-alpha.1 18 | 19 | ## 4.0.0 - 2025-01-24 20 | 21 | #### Changed 22 | - Removed .NET 6.0 as a target framework. 23 | 24 | ## 3.0.2 - 2024-07-22 25 | 26 | #### Changed 27 | - RockLib.Secrets.4.0.0 -> RockLib.Secrets.4.0.1 for vulnerability fix. 28 | 29 | ## 3.0.1 - 2024-03-13 30 | 31 | #### Changed 32 | - Updated RockLib.Secrets NuGet package reference to `4.0.0`. 33 | 34 | ## 3.0.0 - 2024-03-12 35 | 36 | #### Changed 37 | - Finalized 3.0.0 release. 38 | 39 | ## 3.0.0-alpha.1 - 2024-03-12 40 | 41 | #### Changed 42 | - Removed support for .NET Core 3.1 and added .NET 8. 43 | - Updated all NuGet package references to the latest versions. 44 | 45 | ## 2.0.2 - 2023-04-11 46 | 47 | #### Fixed 48 | - Changed the `secretKey` and `secretsManager` parameters to the `AwsSecret` constructor so configuration only requires two values. 49 | 50 | ## 2.0.1 - 2022-07-18 51 | 52 | #### Changed 53 | - Updated the following package references to their latest versions: 54 | - `RockLib.Secrets` - `2.0.2` 55 | - `AWSSDK.SecretsManager` - `3.7.2.65` 56 | 57 | ## 2.0.0 - 2022-04-14 58 | 59 | #### Added 60 | - Added `.editorconfig` and `Directory.Build.props` files to ensure consistency. 61 | 62 | #### Changed 63 | - Supported targets: net6.0, netcoreapp3.1, and net48. 64 | - Updated all NuGet package dependencies to their latest versions: 65 | - `Microsoft.SourceLink.GitHub` - `1.1.1` 66 | - `AWSSDK.SecretsManager` - `3.7.2.29` 67 | - `RockLib.Secrets` - `3.0.0` 68 | - As the package now uses nullable reference types, some method parameters now specify if they can accept nullable values. 69 | - The static field, `AwsSecret.DefaultSecretsManager`, has been removed. Typically this would just end up being an instance of `AmazonSecretsManagerClient`. This also means that `AwsConfigurationBuilderExtensions.SetAmazonSecretsManager` is no longer necessary and has been deleted. If you need to create an instance of `AwsSecret`, you can use an instance of `AmazonSecretsManagerClient` to pass to the `secretsManager` parameter. Having this manager type resolved through a registration in your IoC container is the ideal solution for this. 70 | 71 | ## 1.0.8 - 2021-08-12 72 | 73 | #### Changed 74 | 75 | - Changes "Quicken Loans" to "Rocket Mortgage". 76 | - Updates RockLib.Secrets to latest version, [2.0.6](https://github.com/RockLib/RockLib.Secrets/blob/main/RockLib.Secrets/CHANGELOG.md#206---2021-08-12). 77 | - Updates AWSSDK.SecretsManager to latest version, [3.7.1.4](https://github.com/aws/aws-sdk-net/blob/master/SDK.CHANGELOG.md#37950-2021-08-12-1814-utc). 78 | - Updates Newtonsoft.Json to latest version, [13.0.1](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.1). 79 | 80 | ## 1.0.7 - 2021-05-06 81 | 82 | #### Changed 83 | 84 | - Updates RockLib.Secrets package to latest versions, which include SourceLink. 85 | 86 | ## 1.0.6 - 2021-04-22 87 | 88 | #### Added 89 | 90 | - Adds SourceLink to nuget package. 91 | 92 | ---- 93 | 94 | **Note:** Release notes in the above format are not available for earlier versions of 95 | RockLib.Secrets. What follows below are the original release notes. 96 | 97 | ---- 98 | 99 | ## 1.0.5 100 | 101 | Adds net5.0 target. 102 | 103 | ## 1.0.4 104 | 105 | Adds icon to project and nuget package. 106 | 107 | ## 1.0.3 108 | 109 | Updates to align with nuget conventions. 110 | 111 | ## 1.0.2 112 | 113 | Updates RockLib.Secrets package to latest version. 114 | 115 | ## 1.0.1 116 | 117 | Adds support for plaintext aws secrets. 118 | 119 | ## 1.0.0 120 | 121 | Initial release. 122 | -------------------------------------------------------------------------------- /RockLib.Secrets.Aws/RockLib.Secrets.Aws.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Embedded 4 | An Aws implementation of the ISecretsProvider. 5 | True 6 | True 7 | icon.png 8 | RockLib.Secrets.Aws 9 | LICENSE.md 10 | https://github.com/RockLib/RockLib.Secrets 11 | A changelog is available at https://github.com/RockLib/RockLib.Secrets/blob/main/RockLib.Secrets.Aws/CHANGELOG.md. 12 | false 13 | rocklib secrets aws 14 | 5.0.0 15 | True 16 | 5.0.0 17 | 18 | 19 | bin\$(Configuration)\$(TargetFramework)\$(PackageId).xml 20 | 21 | 22 | true 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /RockLib.Secrets.Aws/RockLib.Secrets.Aws.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29926.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets.Aws", "RockLib.Secrets.Aws.csproj", "{C011E4CB-227F-4369-A6A4-E2880B73F0FA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets.Aws.Tests", "..\Tests\RockLib.Secrets.Aws.Tests\RockLib.Secrets.Aws.Tests.csproj", "{33DF10A6-5E77-4A4F-BD85-CD61F8229376}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C011E4CB-227F-4369-A6A4-E2880B73F0FA}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {33DF10A6-5E77-4A4F-BD85-CD61F8229376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {33DF10A6-5E77-4A4F-BD85-CD61F8229376}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {33DF10A6-5E77-4A4F-BD85-CD61F8229376}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {33DF10A6-5E77-4A4F-BD85-CD61F8229376}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {A43A5F42-B927-48DA-9BC9-D6123CFE9BDC} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /RockLib.Secrets/AssemblyAttributes.cs: -------------------------------------------------------------------------------- 1 | using RockLib.Configuration.ObjectFactory; 2 | using RockLib.Secrets; 3 | using System.Collections.Generic; 4 | 5 | [assembly: ConfigSection("RockLib.Secrets", typeof(List))] -------------------------------------------------------------------------------- /RockLib.Secrets/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # RockLib.Secrets Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 6.0.0 - 2025-02-06 9 | 10 | #### Changed 11 | - Finalized 6.0.0 release. 12 | 13 | ## 6.0.0-alpha.1 - 2025-02-06 14 | 15 | #### Changed 16 | - Updated the following packages: 17 | - RockLib.Configuration from 4.0.3 to 5.0.0 18 | - RockLib.ObjectFactor from 3.0.0 to 4.0.0 19 | 20 | ## 5.0.0 - 2025-01-24 21 | 22 | #### Changed 23 | - Removed .NET 6.0 as a target framework. 24 | - Updated the following packages: 25 | - RockLib.Configuration from 4.0.1 to 4.0.3 26 | 27 | ## 4.0.1 - 2024-07-19 28 | 29 | #### Changed 30 | - RockLib.Configuration.4.0.0 -> RockLib.Configuration.4.0.1 31 | 32 | ## 4.0.0 - 2024-03-12 33 | 34 | #### Changed 35 | - Finalized 4.0.0 release. 36 | 37 | ## 4.0.0-alpha.1 - 2024-03-12 38 | 39 | #### Changed 40 | - Removed support for .NET Core 3.1 and added .NET 8. 41 | - Updated all NuGet package references to the latest versions. 42 | 43 | ## 3.0.1 - 2022-07-18 44 | 45 | #### Changed 46 | - Updated the `RockLib.Configuration.ObjectFactory` package reference to the latest version (`2.0.2`). 47 | 48 | ## 3.0.0 - 2022-04-12 49 | 50 | #### Added 51 | - Added `.editorconfig` and `Directory.Build.props` files to ensure consistency. 52 | 53 | #### Changed 54 | - Supported targets: net6.0, netcoreapp3.1, and net48. 55 | - As the package now uses nullable reference types, some method parameters now specify if they can accept nullable values. 56 | 57 | ## 2.0.6 - 2021-08-12 58 | 59 | #### Changed 60 | 61 | - Changes "Quicken Loans" to "Rocket Mortgage". 62 | - Updates RockLib.Configuration to latest version, [2.5.3](https://github.com/RockLib/RockLib.Configuration/blob/main/RockLib.Configuration/CHANGELOG.md#253---2021-08-11). 63 | - Updates RockLib.Configuration.ObjectFactory to latest version, [1.6.9](https://github.com/RockLib/RockLib.Configuration/blob/main/RockLib.Configuration.ObjectFactory/CHANGELOG.md#169---2021-08-11). 64 | 65 | ## 2.0.5 - 2021-05-06 66 | 67 | #### Changed 68 | 69 | - Updates RockLib.Configuration and RockLib.Configuration.ObjectFactory packages to latest versions, which include SourceLink. 70 | 71 | ## 2.0.4 - 2021-04-22 72 | 73 | #### Added 74 | 75 | - Adds SourceLink to nuget package. 76 | 77 | ---- 78 | 79 | **Note:** Release notes in the above format are not available for earlier versions of 80 | RockLib.Secrets. What follows below are the original release notes. 81 | 82 | ---- 83 | 84 | ## 2.0.3 85 | 86 | Adds net5.0 target. 87 | 88 | ## 2.0.2 89 | 90 | Adds icon to project and nuget package. 91 | 92 | ## 2.0.1 93 | 94 | Updates to align with nuget conventions. 95 | 96 | ## 2.0.0 97 | 98 | - Made configuration extension methods, sources, & providers more idiomatic. 99 | - Added periodic reloading of secrets. 100 | - Added exception handler for when `ISecret.GetValue()` throws an exception. 101 | - Removed the `ISecretsProvider` interface. 102 | - Renamed `ISecret.Key` to `ConfigurationKey`. 103 | - Updated configuration. Instead of the `RockLib_Secrets` / `RockLib.Secrets` composite section defining an `ISecretsProvider`, the section defines one or more instances of `ISecret`. 104 | 105 | ## 1.0.2 106 | 107 | The parameterless overload of the AddRockLibSecrets extension method throws an exception if the configuration does not define a secrets provider in the RockLib_Secrets / RockLib.Secrets composite configuration section or if a secrets provider cannot be created due to invalid configuration. 108 | 109 | ## 1.0.1 110 | 111 | - Adds support for rocklib_secrets config section. 112 | - Adds ConfigSection attribute for the Rockifier tool. 113 | 114 | ## 1.0.0 115 | 116 | Initial release. 117 | -------------------------------------------------------------------------------- /RockLib.Secrets/ConfigurationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using System; 3 | 4 | namespace RockLib.Secrets 5 | { 6 | /// 7 | /// Extension methods for adding . 8 | /// 9 | public static class ConfigurationBuilderExtensions 10 | { 11 | /// 12 | /// The key to the where the 13 | /// default default action to be invoked for SecretsConfigurationProvider when 14 | /// an error occurs while getting the value of a secret is stored. 15 | /// 16 | public const string SecretExceptionHandlerKey = "RockLib.SecretExceptionHandler"; 17 | 18 | /// 19 | /// Adds a secrets configuration source to the , returning 20 | /// an used to define the source's secrets. 21 | /// 22 | /// The to add to. 23 | /// 24 | /// An used to define the source's secrets. 25 | /// 26 | public static ISecretsConfigurationBuilder AddRockLibSecrets(this IConfigurationBuilder builder) => 27 | builder.AddRockLibSecrets(null); 28 | 29 | /// 30 | /// Adds a secrets configuration source to the , returning 31 | /// an used to define the source's secrets. 32 | /// 33 | /// The to add to. 34 | /// 35 | /// Configures the secrets configuration source. Can be . 36 | /// 37 | /// 38 | /// An used to define the source's secrets. 39 | /// 40 | /// 41 | /// Thrown if is null. 42 | /// 43 | public static ISecretsConfigurationBuilder AddRockLibSecrets(this IConfigurationBuilder builder, 44 | Action? configureSource) 45 | { 46 | #if NET6_0_OR_GREATER 47 | ArgumentNullException.ThrowIfNull(builder); 48 | #else 49 | if (builder is null) 50 | { 51 | throw new ArgumentNullException(nameof(builder)); 52 | } 53 | #endif 54 | 55 | var source = new SecretsConfigurationSource(); 56 | configureSource?.Invoke(source); 57 | builder.Add(source); 58 | return new SecretsConfigurationBuilder(source); 59 | } 60 | 61 | /// 62 | /// Sets a default action to be invoked for SecretsConfigurationProvider when an error occurs while getting 63 | /// the value of a secret. 64 | /// 65 | /// The to add to. 66 | /// The action to be invoked for SecretsConfigurationProvider. 67 | /// The . 68 | /// 69 | /// Thrown if or is null. 70 | /// 71 | public static IConfigurationBuilder SetSecretExceptionHandler(this IConfigurationBuilder builder, 72 | Action onSecretException) 73 | { 74 | #if NET6_0_OR_GREATER 75 | ArgumentNullException.ThrowIfNull(builder); 76 | ArgumentNullException.ThrowIfNull(onSecretException); 77 | #else 78 | if (builder is null) 79 | { 80 | throw new ArgumentNullException(nameof(builder)); 81 | } 82 | 83 | if(onSecretException is null) 84 | { 85 | throw new ArgumentNullException(nameof(onSecretException)); 86 | } 87 | #endif 88 | 89 | builder.Properties[SecretExceptionHandlerKey] = onSecretException; 90 | return builder; 91 | } 92 | 93 | /// 94 | /// Gets the default action to be invoked for SecretsConfigurationProvider when an error occurs while 95 | /// getting the value of a secret. 96 | /// 97 | /// The . 98 | /// 99 | /// The default action to be invoked for SecretsConfigurationProvider when an error occurs while 100 | /// getting the value of a secret, or null otherwise. 101 | /// 102 | /// 103 | /// Thrown if is null. 104 | /// 105 | public static Action? GetSecretExceptionHandler(this IConfigurationBuilder builder) 106 | { 107 | #if NET6_0_OR_GREATER 108 | ArgumentNullException.ThrowIfNull(builder); 109 | #else 110 | if (builder is null) 111 | { 112 | throw new ArgumentNullException(nameof(builder)); 113 | } 114 | #endif 115 | 116 | if (builder.Properties.TryGetValue(SecretExceptionHandlerKey, out var value) && value is Action exceptionHandler) 117 | { 118 | return exceptionHandler; 119 | } 120 | 121 | return null; 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /RockLib.Secrets/ISecret.cs: -------------------------------------------------------------------------------- 1 | namespace RockLib.Secrets 2 | { 3 | /// 4 | /// Defines a secret. 5 | /// 6 | public interface ISecret 7 | { 8 | /// 9 | /// Gets the configuration key for the secret. 10 | /// 11 | string ConfigurationKey { get; } 12 | 13 | /// 14 | /// Gets the value of the secret. 15 | /// 16 | /// The secret value. 17 | string GetValue(); 18 | } 19 | } -------------------------------------------------------------------------------- /RockLib.Secrets/ISecretsConfigurationBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace RockLib.Secrets 2 | { 3 | /// 4 | /// Defines a builder for adding secrets. 5 | /// 6 | public interface ISecretsConfigurationBuilder 7 | { 8 | /// 9 | /// Adds a secret to the builder. 10 | /// 11 | /// The . 12 | /// The . 13 | ISecretsConfigurationBuilder AddSecret(ISecret secret); 14 | } 15 | } -------------------------------------------------------------------------------- /RockLib.Secrets/RockLib.Secrets.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Embedded 4 | A configuration provider for Microsoft.Extensions.Configuration that is backed by runtime secrets. 5 | True 6 | True 7 | icon.png 8 | RockLib.Secrets 9 | LICENSE.md 10 | https://github.com/RockLib/RockLib.Secrets 11 | A changelog is available at https://github.com/RockLib/RockLib.Secrets/blob/main/RockLib.Secrets/CHANGELOG.md. 12 | false 13 | rocklib secrets configuration 14 | 6.0.0 15 | True 16 | 6.0.0 17 | 18 | 19 | bin\$(Configuration)\$(TargetFramework)\$(PackageId).xml 20 | 21 | 22 | true 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /RockLib.Secrets/RockLib.Secrets.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29926.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets", "RockLib.Secrets.csproj", "{6E303E27-044B-420D-AD41-D07959C9E32F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RockLib.Secrets.Tests", "..\Tests\RockLib.Secrets.Tests\RockLib.Secrets.Tests.csproj", "{AD3162D0-B27E-466C-B0A4-995FAC4B68EC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {6E303E27-044B-420D-AD41-D07959C9E32F}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {AD3162D0-B27E-466C-B0A4-995FAC4B68EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {AD3162D0-B27E-466C-B0A4-995FAC4B68EC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {AD3162D0-B27E-466C-B0A4-995FAC4B68EC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {AD3162D0-B27E-466C-B0A4-995FAC4B68EC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {A43A5F42-B927-48DA-9BC9-D6123CFE9BDC} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /RockLib.Secrets/SecretExceptionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RockLib.Secrets 4 | { 5 | /// 6 | /// Contains information about a secret exception. 7 | /// 8 | public class SecretExceptionContext 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// 14 | /// The that invoked the 15 | /// method that caused the exception. 16 | /// 17 | /// The that caused the exception. 18 | /// The exception that occurred in . 19 | /// 20 | /// Thrown if or or is null. 21 | /// 22 | public SecretExceptionContext(SecretsConfigurationProvider provider, ISecret secret, Exception exception) 23 | { 24 | Provider = provider ?? throw new ArgumentNullException(nameof(provider)); 25 | Secret = secret ?? throw new ArgumentNullException(nameof(secret)); 26 | Exception = exception ?? throw new ArgumentNullException(nameof(exception)); 27 | } 28 | 29 | /// 30 | /// The that invoked the 31 | /// method that caused the exception. 32 | /// 33 | public SecretsConfigurationProvider Provider { get; } 34 | 35 | /// 36 | /// The that caused the exception. 37 | /// 38 | public ISecret Secret { get; } 39 | 40 | /// 41 | /// The exception that occurred in . 42 | /// 43 | public Exception Exception { get; } 44 | } 45 | } -------------------------------------------------------------------------------- /RockLib.Secrets/SecretsConfigurationBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RockLib.Secrets 4 | { 5 | /// 6 | /// A builder object that adds secrets to a secrets source. 7 | /// 8 | public class SecretsConfigurationBuilder : ISecretsConfigurationBuilder 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The secrets configuration source. 14 | /// 15 | /// Thrown if is null. 16 | /// 17 | public SecretsConfigurationBuilder(SecretsConfigurationSource source) 18 | { 19 | Source = source ?? throw new ArgumentNullException(nameof(source)); 20 | } 21 | 22 | /// 23 | /// The source backing the builder. 24 | /// 25 | public SecretsConfigurationSource Source { get; } 26 | 27 | /// 28 | /// Adds a secret to the secrets source. 29 | /// 30 | /// The . 31 | /// The . 32 | /// 33 | /// Thrown if is null. 34 | /// 35 | public ISecretsConfigurationBuilder AddSecret(ISecret secret) 36 | { 37 | Source.Secrets.Add(secret ?? throw new ArgumentNullException(nameof(secret))); 38 | return this; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /RockLib.Secrets/SecretsConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Linq; 6 | using System.Threading; 7 | 8 | namespace RockLib.Secrets 9 | { 10 | /// 11 | /// Implementation of backed by a collection of secrets. 12 | /// 13 | public class SecretsConfigurationProvider : ConfigurationProvider, IDisposable 14 | { 15 | private readonly Timer _timer; 16 | private bool _disposed; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The source settings. 22 | /// 23 | /// Thrown if is null 24 | /// 25 | /// 26 | /// Thrown if the secrets collection has any unexpected issues. 27 | /// 28 | public SecretsConfigurationProvider(SecretsConfigurationSource source) 29 | { 30 | Source = source ?? throw new ArgumentNullException(nameof(source)); 31 | Secrets = new ReadOnlyCollection(source.Secrets.ToArray()); 32 | _timer = new Timer(_ => Load()); 33 | 34 | if (!Secrets.Any()) 35 | { 36 | throw new ArgumentException("The SecretsConfigurationSource must contain at least one secret.", nameof(source)); 37 | } 38 | if (Secrets.Any(s => s is null)) 39 | { 40 | throw new ArgumentException("The SecretsConfigurationSource cannot contain any null secrets.", nameof(source)); 41 | } 42 | if (Secrets.Any(s => s.ConfigurationKey is null)) 43 | { 44 | throw new ArgumentException("The SecretsConfigurationSource cannot contain any secrets with a null Key.", nameof(source)); 45 | } 46 | if (Secrets.Select(s => s.ConfigurationKey).Distinct(StringComparer.OrdinalIgnoreCase).Count() != Secrets.Count) 47 | { 48 | throw new ArgumentException("The SecretsConfigurationSource cannot contain any secrets with duplicate Keys.", nameof(source)); 49 | } 50 | } 51 | 52 | /// 53 | /// The source settings for this provider. 54 | /// 55 | public SecretsConfigurationSource Source { get; } 56 | 57 | /// 58 | /// The secrets that define this provider. 59 | /// 60 | public IReadOnlyList Secrets { get; } 61 | 62 | /// 63 | /// Loads data from . 64 | /// 65 | public override void Load() 66 | { 67 | try 68 | { 69 | var secrets = Secrets.Select(secret => 70 | { 71 | try 72 | { 73 | return new { secret.ConfigurationKey, Value = secret.GetValue() }; 74 | } 75 | // Would love to catch a specific exception, but... 76 | // we don't know what the implementation of ISecret.GetValue() 77 | // will do here, and this code has already been published, so... 78 | // we have to keep this as a general Exception. 79 | #pragma warning disable CA1031 // Do not catch general exception types 80 | catch (Exception ex) 81 | #pragma warning restore CA1031 // Do not catch general exception types 82 | { 83 | Source.OnSecretException?.Invoke(new SecretExceptionContext(this, secret, ex)); 84 | return new { secret.ConfigurationKey, Value = (string)null! }; 85 | } 86 | }); 87 | 88 | var secretChanged = false; 89 | 90 | if (Data.Count == 0) 91 | { 92 | foreach (var secret in secrets) 93 | { 94 | Data.Add(secret.ConfigurationKey, secret.Value); 95 | } 96 | } 97 | else 98 | { 99 | foreach (var secret in secrets) 100 | { 101 | if (secret.Value is not null && Data[secret.ConfigurationKey] != secret.Value) 102 | { 103 | secretChanged = true; 104 | Data[secret.ConfigurationKey] = secret.Value; 105 | } 106 | } 107 | } 108 | 109 | if (secretChanged) 110 | { 111 | OnReload(); 112 | } 113 | } 114 | finally 115 | { 116 | _timer.Change(Source.ReloadMilliseconds, Timeout.Infinite); 117 | } 118 | } 119 | 120 | /// 121 | /// Disposes the unmanaged resources. 122 | /// 123 | ~SecretsConfigurationProvider() 124 | { 125 | Dispose(false); 126 | } 127 | 128 | /// 129 | /// Disposes the object. 130 | /// 131 | public void Dispose() 132 | { 133 | Dispose(true); 134 | GC.SuppressFinalize(this); 135 | } 136 | 137 | /// 138 | /// Disposes the object 139 | /// 140 | /// Specifies if this is a managed or unmanaged disposal 141 | protected virtual void Dispose(bool dispose) 142 | { 143 | if(!_disposed) 144 | { 145 | if(dispose) 146 | { 147 | _timer.Dispose(); 148 | } 149 | } 150 | _disposed = true; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /RockLib.Secrets/SecretsConfigurationSource.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using RockLib.Configuration; 3 | using RockLib.Configuration.ObjectFactory; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading; 8 | 9 | namespace RockLib.Secrets 10 | { 11 | /// 12 | /// Represents a source of secrets as an . 13 | /// 14 | public class SecretsConfigurationSource : IConfigurationSource 15 | { 16 | /// The default value of the property, 5 minutes. 17 | public const int DefaultReloadMilliseconds = 5 * 60 * 1000; 18 | 19 | private int _reloadMilliseconds = DefaultReloadMilliseconds; 20 | private bool _alreadyBuilt; 21 | 22 | /// 23 | /// Gets the list of 24 | /// 25 | public IList Secrets { get; } = new List(); 26 | 27 | /// 28 | /// Will be called if an uncaught exception occurs when calling 29 | /// from the method. 30 | /// 31 | public Action? OnSecretException { get; set; } 32 | 33 | /// 34 | /// The number of milliseconds to wait before reloading secrets. Specify 35 | /// to disable reloading. 36 | /// 37 | public int ReloadMilliseconds 38 | { 39 | get => _reloadMilliseconds; 40 | set 41 | { 42 | if (value < Timeout.Infinite) 43 | { 44 | throw new ArgumentOutOfRangeException(nameof(value), "Must be either non-negative or -1."); 45 | } 46 | _reloadMilliseconds = value; 47 | } 48 | } 49 | 50 | /// 51 | /// Disables periodic reloading. 52 | /// 53 | public void DisableReload() => ReloadMilliseconds = Timeout.Infinite; 54 | 55 | /// 56 | /// Builds the for this source. 57 | /// 58 | /// The . 59 | /// An instance of . 60 | public IConfigurationProvider Build(IConfigurationBuilder builder) 61 | { 62 | #if NET6_0_OR_GREATER 63 | ArgumentNullException.ThrowIfNull(builder); 64 | #else 65 | if (builder is null) 66 | { 67 | throw new ArgumentNullException(nameof(builder)); 68 | } 69 | #endif 70 | 71 | if (!_alreadyBuilt) 72 | { 73 | _alreadyBuilt = true; 74 | foreach (var secretFromConfiguration in CreateSecretsFromConfiguration(builder)) 75 | { 76 | Secrets.Add(secretFromConfiguration); 77 | } 78 | } 79 | 80 | OnSecretException ??= builder.GetSecretExceptionHandler(); 81 | 82 | return new SecretsConfigurationProvider(this); 83 | } 84 | 85 | private List CreateSecretsFromConfiguration(IConfigurationBuilder builder) 86 | { 87 | // Make a copy of the builder, excluding this SecretsConfigurationSource. 88 | // Otherwise there will be infinite recursion when building the builder. 89 | var builderCopy = new ConfigurationBuilder(); 90 | 91 | foreach (var source in builder.Sources.Where(s => !ReferenceEquals(this, s))) 92 | { 93 | builderCopy.Add(source); 94 | } 95 | 96 | foreach (var property in builder.Properties) 97 | { 98 | builderCopy.Properties.Add(property.Key, property.Value); 99 | } 100 | 101 | var configuration = builderCopy.Build(); 102 | 103 | return configuration.GetCompositeSection("RockLib_Secrets", "RockLib.Secrets") 104 | .Create>()!; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Aws.Tests/AssemblySettings.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Aws.Tests/AwsSecretTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using Amazon.SecretsManager; 3 | using Amazon.SecretsManager.Model; 4 | using FluentAssertions; 5 | using Moq; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Text; 10 | using System.Threading; 11 | using Xunit; 12 | 13 | namespace RockLib.Secrets.Aws.Tests; 14 | 15 | public static class AwsSecretTests 16 | { 17 | [Fact] 18 | public static void Create() 19 | { 20 | var secretsManager = new Mock().Object; 21 | 22 | var secret = new AwsSecret("configurationKey", "secretId", "secretKey", secretsManager); 23 | 24 | secret.ConfigurationKey.Should().Be("configurationKey"); 25 | secret.SecretId.Should().Be("secretId"); 26 | secret.SecretKey.Should().Be("secretKey"); 27 | secret.SecretsManager.Should().BeSameAs(secretsManager); 28 | } 29 | 30 | [Fact] 31 | public static void CreateWithNullSecretKey() 32 | { 33 | var secretsManager = new Mock().Object; 34 | 35 | var secret = new AwsSecret("configurationKey", "secretId", null, secretsManager); 36 | 37 | secret.ConfigurationKey.Should().Be("configurationKey"); 38 | secret.SecretId.Should().Be("secretId"); 39 | secret.SecretKey.Should().BeNull(); 40 | secret.SecretsManager.Should().BeSameAs(secretsManager); 41 | } 42 | 43 | [Fact] 44 | public static void CreateWithNullManager() 45 | { 46 | var secret = new AwsSecret("configurationKey", "secretId", "secretKey", null); 47 | 48 | secret.ConfigurationKey.Should().Be("configurationKey"); 49 | secret.SecretId.Should().Be("secretId"); 50 | secret.SecretKey.Should().Be("secretKey"); 51 | secret.SecretsManager.Should().BeNull(); 52 | } 53 | 54 | [Fact] 55 | public static void CreateWithNoSecretKeyAndManager() 56 | { 57 | var secret = new AwsSecret("configurationKey", "secretId"); 58 | 59 | secret.ConfigurationKey.Should().Be("configurationKey"); 60 | secret.SecretId.Should().Be("secretId"); 61 | secret.SecretKey.Should().BeNull(); 62 | secret.SecretsManager.Should().BeNull(); 63 | } 64 | 65 | [Fact] 66 | public static void CreateWithNullConfigurationKey() 67 | { 68 | var act = () => new AwsSecret(null!, "secretId", null, Mock.Of()); 69 | 70 | act.Should().ThrowExactly().WithMessage("*configurationKey*"); 71 | } 72 | 73 | [Fact] 74 | public static void CreateWithNullSecretId() 75 | { 76 | var act = () => new AwsSecret("configurationKey", null!, null, Mock.Of()); 77 | 78 | act.Should().ThrowExactly().WithMessage("*secretId*"); 79 | } 80 | 81 | [Fact] 82 | public static void GetValue() 83 | { 84 | var response = new GetSecretValueResponse 85 | { 86 | SecretString = "{'myAwsSecretKey':'secretValue'}" 87 | }; 88 | 89 | var mockSecretsManager = new Mock(); 90 | mockSecretsManager.Setup(m => m.GetSecretValueAsync(It.IsAny(), It.IsAny())) 91 | .ReturnsAsync(response); 92 | 93 | var secret = new AwsSecret("myConfigurationKey", "mySecretId", "myAwsSecretKey", mockSecretsManager.Object); 94 | 95 | var value = secret.GetValue(); 96 | 97 | value.Should().Be("secretValue"); 98 | 99 | mockSecretsManager.Verify(m => m.GetSecretValueAsync( 100 | It.Is(r => r.SecretId == "mySecretId"), It.IsAny()), 101 | Times.Once()); 102 | } 103 | 104 | [Fact] 105 | public static void GetValueWhenSecretKeyIsNotSupplied() 106 | { 107 | var response = new GetSecretValueResponse 108 | { 109 | SecretString = "mySecretString" 110 | }; 111 | 112 | var mockSecretsManager = new Mock(); 113 | mockSecretsManager.Setup(m => m.GetSecretValueAsync(It.IsAny(), It.IsAny())) 114 | .ReturnsAsync(response); 115 | 116 | var secret = new AwsSecret("myConfigurationKey", "mySecretId", null, mockSecretsManager.Object); 117 | 118 | var value = secret.GetValue(); 119 | 120 | value.Should().Be("mySecretString"); 121 | 122 | mockSecretsManager.Verify(m => m.GetSecretValueAsync( 123 | It.Is(r => r.SecretId == "mySecretId"), It.IsAny()), 124 | Times.Once()); 125 | } 126 | 127 | [Fact] 128 | public static void GetValueWhenSecretStringIsNull() 129 | { 130 | var buffer = Encoding.UTF8.GetBytes("Hello, world!"); 131 | 132 | var response = new GetSecretValueResponse 133 | { 134 | SecretBinary = new MemoryStream(buffer) 135 | }; 136 | 137 | var mockSecretsManager = new Mock(); 138 | mockSecretsManager.Setup(m => m.GetSecretValueAsync(It.IsAny(), It.IsAny())) 139 | .ReturnsAsync(response); 140 | 141 | var secret = new AwsSecret("myConfigurationKey", "mySecretId", null, mockSecretsManager.Object); 142 | 143 | var value = secret.GetValue(); 144 | 145 | value.Should().Be(Convert.ToBase64String(buffer)); 146 | 147 | mockSecretsManager.Verify(m => m.GetSecretValueAsync( 148 | It.Is(r => r.SecretId == "mySecretId"), It.IsAny()), 149 | Times.Once()); 150 | } 151 | 152 | [Fact] 153 | public static void GetValueWithNullResponseFromSecretsManager() 154 | { 155 | var mockSecretsManager = new Mock(); 156 | mockSecretsManager.Setup(m => m.GetSecretValueAsync(It.IsAny(), It.IsAny())) 157 | .ReturnsAsync((GetSecretValueResponse)null!); 158 | 159 | var secret = new AwsSecret("myConfigurationKey", "mySecretId", "myAwsSecretKey", mockSecretsManager.Object); 160 | 161 | var act = () => secret.GetValue(); 162 | 163 | act.Should().ThrowExactly().WithMessage("*Response was null.*"); 164 | } 165 | 166 | [Fact] 167 | public static void GetValueWhenSecretStringDoesNotContainSecretKey() 168 | { 169 | var response = new GetSecretValueResponse 170 | { 171 | SecretString = "{}" 172 | }; 173 | 174 | var mockSecretsManager = new Mock(); 175 | mockSecretsManager.Setup(m => m.GetSecretValueAsync(It.IsAny(), It.IsAny())) 176 | .ReturnsAsync(response); 177 | 178 | var secret = new AwsSecret("myConfigurationKey", "mySecretId", "myAwsSecretKey", mockSecretsManager.Object); 179 | 180 | var act = () => secret.GetValue(); 181 | 182 | act.Should().ThrowExactly().WithMessage("*Response did not contain item with the name 'myAwsSecretKey'.*"); 183 | } 184 | 185 | [Fact] 186 | public static void GetValueWhenSecretStringandSecretBinaryAreNull() 187 | { 188 | var response = new GetSecretValueResponse(); 189 | 190 | var mockSecretsManager = new Mock(); 191 | mockSecretsManager.Setup(m => m.GetSecretValueAsync(It.IsAny(), It.IsAny())) 192 | .ReturnsAsync(response); 193 | 194 | var secret = new AwsSecret("myConfigurationKey", "mySecretId", "myAwsSecretKey", mockSecretsManager.Object); 195 | 196 | var act = () => secret.GetValue(); 197 | 198 | act.Should().ThrowExactly().WithMessage("*Response did not contain a value for SecretString or SecretBinary.*"); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Aws.Tests/AwsSecretsConfigurationBuilderExtensionsTests .cs: -------------------------------------------------------------------------------- 1 | using Amazon.SecretsManager; 2 | using FluentAssertions; 3 | using Microsoft.Extensions.Configuration; 4 | using Moq; 5 | using System; 6 | using Xunit; 7 | 8 | namespace RockLib.Secrets.Aws.Tests; 9 | 10 | public static class AwsSecretsConfigurationBuilderExtensionsTests 11 | { 12 | [Fact] 13 | public static void AddAwsSecret() 14 | { 15 | ISecret secret = null!; 16 | var builderMock = new Mock(); 17 | builderMock 18 | .Setup(bm => bm.AddSecret(It.IsAny())) 19 | .Callback(s => secret = s); 20 | 21 | var configurationKey = "configurationKey"; 22 | var secretId = "secretId"; 23 | var secretKey = "secretKey"; 24 | var awsSecretsManager = new Mock().Object; 25 | 26 | builderMock.Object.AddAwsSecret(configurationKey, secretId, secretKey, awsSecretsManager); 27 | 28 | builderMock.Verify(); 29 | 30 | var awsSecret = secret.Should().BeOfType().Subject; 31 | awsSecret.ConfigurationKey.Should().Be(configurationKey); 32 | awsSecret.SecretId.Should().Be(secretId); 33 | awsSecret.SecretKey.Should().Be(secretKey); 34 | awsSecret.SecretsManager.Should().BeSameAs(awsSecretsManager); 35 | } 36 | 37 | [Fact] 38 | public static void AddAwsSecretWithNullBuilder() 39 | { 40 | var configurationKey = "configurationKey"; 41 | var secretId = "secretId"; 42 | var secretKey = "secretKey"; 43 | var awsSecretsManager = new Mock().Object; 44 | 45 | ISecretsConfigurationBuilder builder = null!; 46 | 47 | Action act = () => builder.AddAwsSecret(configurationKey, secretId, secretKey, awsSecretsManager); 48 | 49 | act.Should().ThrowExactly().WithMessage("*builder*"); 50 | } 51 | 52 | [Fact] 53 | public static void AddAwsSecretsWithRockLibConfiguration() 54 | { 55 | var configurationBuilder = new ConfigurationBuilder(); 56 | configurationBuilder.AddJsonFile("appsettings.json") 57 | .AddRockLibSecrets(); 58 | var buildAction = () => configurationBuilder.Build(); 59 | buildAction.Should().NotThrow(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Aws.Tests/FakeSecretsManager.cs: -------------------------------------------------------------------------------- 1 | using Amazon.Runtime; 2 | using Amazon.Runtime.Endpoints; 3 | using Amazon.SecretsManager; 4 | using Amazon.SecretsManager.Model; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace RockLib.Secrets.Aws.Tests; 10 | 11 | // Defined purely for the appsettings.json file 12 | // for testing. 13 | #pragma warning disable CA1065 14 | public sealed class FakeSecretsManager 15 | : IAmazonSecretsManager 16 | { 17 | public ISecretsManagerPaginatorFactory Paginators => throw new NotImplementedException(); 18 | 19 | public IClientConfig Config => throw new NotImplementedException(); 20 | 21 | public Task CancelRotateSecretAsync(CancelRotateSecretRequest request, CancellationToken cancellationToken = default) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | 26 | public CancelRotateSecretResponse CancelRotateSecret(CancelRotateSecretRequest request) 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | 31 | public Task CreateSecretAsync(CreateSecretRequest request, CancellationToken cancellationToken = default) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | public CreateSecretResponse CreateSecret(CreateSecretRequest request) 37 | { 38 | throw new NotImplementedException(); 39 | } 40 | 41 | public Task DeleteResourcePolicyAsync(DeleteResourcePolicyRequest request, CancellationToken cancellationToken = default) 42 | { 43 | throw new NotImplementedException(); 44 | } 45 | 46 | public DeleteResourcePolicyResponse DeleteResourcePolicy(DeleteResourcePolicyRequest request) 47 | { 48 | throw new NotImplementedException(); 49 | } 50 | 51 | public Task DeleteSecretAsync(DeleteSecretRequest request, CancellationToken cancellationToken = default) 52 | { 53 | throw new NotImplementedException(); 54 | } 55 | 56 | public DeleteSecretResponse DeleteSecret(DeleteSecretRequest request) 57 | { 58 | throw new NotImplementedException(); 59 | } 60 | 61 | public Task DescribeSecretAsync(DescribeSecretRequest request, CancellationToken cancellationToken = default) 62 | { 63 | throw new NotImplementedException(); 64 | } 65 | 66 | public DescribeSecretResponse DescribeSecret(DescribeSecretRequest request) 67 | { 68 | throw new NotImplementedException(); 69 | } 70 | 71 | public void Dispose() 72 | { 73 | throw new NotImplementedException(); 74 | } 75 | 76 | public Task GetRandomPasswordAsync(GetRandomPasswordRequest request, CancellationToken cancellationToken = default) 77 | { 78 | throw new NotImplementedException(); 79 | } 80 | 81 | public GetRandomPasswordResponse GetRandomPassword(GetRandomPasswordRequest request) 82 | { 83 | throw new NotImplementedException(); 84 | } 85 | 86 | public Task GetResourcePolicyAsync(GetResourcePolicyRequest request, CancellationToken cancellationToken = default) 87 | { 88 | throw new NotImplementedException(); 89 | } 90 | 91 | public GetResourcePolicyResponse GetResourcePolicy(GetResourcePolicyRequest request) 92 | { 93 | throw new NotImplementedException(); 94 | } 95 | 96 | public Task GetSecretValueAsync(GetSecretValueRequest request, CancellationToken cancellationToken = default) 97 | { 98 | throw new NotImplementedException(); 99 | } 100 | 101 | public GetSecretValueResponse GetSecretValue(GetSecretValueRequest request) 102 | { 103 | throw new NotImplementedException(); 104 | } 105 | 106 | public Task ListSecretsAsync(ListSecretsRequest request, CancellationToken cancellationToken = default) 107 | { 108 | throw new NotImplementedException(); 109 | } 110 | 111 | public ListSecretsResponse ListSecrets(ListSecretsRequest request) 112 | { 113 | throw new NotImplementedException(); 114 | } 115 | 116 | public Task ListSecretVersionIdsAsync(ListSecretVersionIdsRequest request, CancellationToken cancellationToken = default) 117 | { 118 | throw new NotImplementedException(); 119 | } 120 | 121 | public ListSecretVersionIdsResponse ListSecretVersionIds(ListSecretVersionIdsRequest request) 122 | { 123 | throw new NotImplementedException(); 124 | } 125 | 126 | public Task PutResourcePolicyAsync(PutResourcePolicyRequest request, CancellationToken cancellationToken = default) 127 | { 128 | throw new NotImplementedException(); 129 | } 130 | 131 | public PutResourcePolicyResponse PutResourcePolicy(PutResourcePolicyRequest request) 132 | { 133 | throw new NotImplementedException(); 134 | } 135 | 136 | public Task PutSecretValueAsync(PutSecretValueRequest request, CancellationToken cancellationToken = default) 137 | { 138 | throw new NotImplementedException(); 139 | } 140 | 141 | public PutSecretValueResponse PutSecretValue(PutSecretValueRequest request) 142 | { 143 | throw new NotImplementedException(); 144 | } 145 | 146 | public Task RemoveRegionsFromReplicationAsync(RemoveRegionsFromReplicationRequest request, CancellationToken cancellationToken = default) 147 | { 148 | throw new NotImplementedException(); 149 | } 150 | 151 | public RemoveRegionsFromReplicationResponse RemoveRegionsFromReplication(RemoveRegionsFromReplicationRequest request) 152 | { 153 | throw new NotImplementedException(); 154 | } 155 | 156 | public Task ReplicateSecretToRegionsAsync(ReplicateSecretToRegionsRequest request, CancellationToken cancellationToken = default) 157 | { 158 | throw new NotImplementedException(); 159 | } 160 | 161 | public ReplicateSecretToRegionsResponse ReplicateSecretToRegions(ReplicateSecretToRegionsRequest request) 162 | { 163 | throw new NotImplementedException(); 164 | } 165 | 166 | public Task RestoreSecretAsync(RestoreSecretRequest request, CancellationToken cancellationToken = default) 167 | { 168 | throw new NotImplementedException(); 169 | } 170 | 171 | public RestoreSecretResponse RestoreSecret(RestoreSecretRequest request) 172 | { 173 | throw new NotImplementedException(); 174 | } 175 | 176 | public Task RotateSecretAsync(RotateSecretRequest request, CancellationToken cancellationToken = default) 177 | { 178 | throw new NotImplementedException(); 179 | } 180 | 181 | public RotateSecretResponse RotateSecret(RotateSecretRequest request) 182 | { 183 | throw new NotImplementedException(); 184 | } 185 | 186 | public Task StopReplicationToReplicaAsync(StopReplicationToReplicaRequest request, CancellationToken cancellationToken = default) 187 | { 188 | throw new NotImplementedException(); 189 | } 190 | 191 | public StopReplicationToReplicaResponse StopReplicationToReplica(StopReplicationToReplicaRequest request) 192 | { 193 | throw new NotImplementedException(); 194 | } 195 | 196 | public Task TagResourceAsync(TagResourceRequest request, CancellationToken cancellationToken = default) 197 | { 198 | throw new NotImplementedException(); 199 | } 200 | 201 | public TagResourceResponse TagResource(TagResourceRequest request) 202 | { 203 | throw new NotImplementedException(); 204 | } 205 | 206 | public Task UntagResourceAsync(UntagResourceRequest request, CancellationToken cancellationToken = default) 207 | { 208 | throw new NotImplementedException(); 209 | } 210 | 211 | public UntagResourceResponse UntagResource(UntagResourceRequest request) 212 | { 213 | throw new NotImplementedException(); 214 | } 215 | 216 | public Task UpdateSecretAsync(UpdateSecretRequest request, CancellationToken cancellationToken = default) 217 | { 218 | throw new NotImplementedException(); 219 | } 220 | 221 | public UpdateSecretResponse UpdateSecret(UpdateSecretRequest request) 222 | { 223 | throw new NotImplementedException(); 224 | } 225 | 226 | public Task UpdateSecretVersionStageAsync(UpdateSecretVersionStageRequest request, CancellationToken cancellationToken = default) 227 | { 228 | throw new NotImplementedException(); 229 | } 230 | 231 | public UpdateSecretVersionStageResponse UpdateSecretVersionStage(UpdateSecretVersionStageRequest request) 232 | { 233 | throw new NotImplementedException(); 234 | } 235 | 236 | public Task ValidateResourcePolicyAsync(ValidateResourcePolicyRequest request, CancellationToken cancellationToken = default) 237 | { 238 | throw new NotImplementedException(); 239 | } 240 | 241 | public ValidateResourcePolicyResponse ValidateResourcePolicy(ValidateResourcePolicyRequest request) 242 | { 243 | throw new NotImplementedException(); 244 | } 245 | 246 | public Task BatchGetSecretValueAsync(BatchGetSecretValueRequest request, CancellationToken cancellationToken = default) 247 | { 248 | throw new NotImplementedException(); 249 | } 250 | 251 | public Endpoint DetermineServiceOperationEndpoint(AmazonWebServiceRequest request) 252 | { 253 | throw new NotImplementedException(); 254 | } 255 | 256 | public BatchGetSecretValueResponse BatchGetSecretValue(BatchGetSecretValueRequest request) 257 | { 258 | throw new NotImplementedException(); 259 | } 260 | } 261 | #pragma warning restore CA1065 -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Aws.Tests/RockLib.Secrets.Aws.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | PreserveNewest 11 | true 12 | PreserveNewest 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Aws.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllSettings": "", 3 | "NoSecretKey": "", 4 | "NoSecretsManager": "", 5 | "NoSecretKeyAndSecretsManager": "", 6 | 7 | "RockLib.Secrets": [ 8 | { 9 | "Type": "RockLib.Secrets.Aws.AwsSecret, RockLib.Secrets.Aws", 10 | "Value": { 11 | "ConfigurationKey": "AllSettings", 12 | "SecretId": "RockLib.Example.Secret", 13 | "SecretKey": "MySecretKey", 14 | "SecretsManager": { 15 | "Type": "RockLib.Secrets.Aws.Tests.FakeSecretsManager, RockLib.Secrets.Aws.Tests" 16 | } 17 | } 18 | }, 19 | { 20 | "Type": "RockLib.Secrets.Aws.AwsSecret, RockLib.Secrets.Aws", 21 | "Value": { 22 | "ConfigurationKey": "NoSecretKey", 23 | "SecretId": "RockLib.Example.Secret", 24 | "SecretsManager": { 25 | "Type": "RockLib.Secrets.Aws.Tests.FakeSecretsManager, RockLib.Secrets.Aws.Tests" 26 | } 27 | } 28 | }, 29 | { 30 | "Type": "RockLib.Secrets.Aws.AwsSecret, RockLib.Secrets.Aws", 31 | "Value": { 32 | "ConfigurationKey": "NoSecretsManager", 33 | "SecretId": "RockLib.Example.Secret", 34 | "SecretKey": "MySecretKey" 35 | } 36 | }, 37 | { 38 | "Type": "RockLib.Secrets.Aws.AwsSecret, RockLib.Secrets.Aws", 39 | "Value": { 40 | "ConfigurationKey": "NoSecretKeyAndSecretsManager", 41 | "SecretId": "RockLib.Example.Secret" 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Tests/AssemblySettings.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Tests/ConfigurationBuilderExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.Extensions.Configuration; 3 | using System; 4 | using Xunit; 5 | 6 | namespace RockLib.Secrets.Tests 7 | { 8 | public static class ConfigurationBuilderExtensionsTests 9 | { 10 | [Fact] 11 | public static void AddRockLibSecrets() 12 | { 13 | var builder = new ConfigurationBuilder(); 14 | 15 | builder.Sources.Should().BeEmpty(); 16 | 17 | var secretsBuilder = builder.AddRockLibSecrets(); 18 | 19 | builder.Sources.Should().ContainSingle(); 20 | builder.Sources[0].Should().BeOfType(); 21 | 22 | secretsBuilder.Should().BeOfType() 23 | .Which.Source.Should().BeSameAs(builder.Sources[0]); 24 | } 25 | 26 | [Fact] 27 | public static void AddRockLibSecretsWithNullBuilder() 28 | { 29 | IConfigurationBuilder builder = null!; 30 | 31 | var act = () => builder.AddRockLibSecrets(); 32 | 33 | act.Should().ThrowExactly().WithMessage("*builder*"); 34 | } 35 | 36 | [Fact] 37 | public static void AddRockLibSecretsWithConfiguration() 38 | { 39 | var builder = new ConfigurationBuilder(); 40 | 41 | builder.Sources.Should().BeEmpty(); 42 | 43 | var secret = MockSecret.Get("foo", "bar").Object; 44 | 45 | Action configureSource = source => source.Secrets.Add(secret); 46 | 47 | var secretsBuilder = builder.AddRockLibSecrets(configureSource); 48 | 49 | builder.Sources.Should().ContainSingle(); 50 | builder.Sources[0].Should().BeOfType() 51 | .Which.Secrets.Should().ContainSingle(s => ReferenceEquals(s, secret)); 52 | 53 | secretsBuilder.Should().BeOfType() 54 | .Which.Source.Should().BeSameAs(builder.Sources[0]); 55 | } 56 | 57 | [Fact] 58 | public static void AddRockLibSecretsWithSourceAndNullBuilder() 59 | { 60 | IConfigurationBuilder builder = null!; 61 | 62 | Action act = () => builder.AddRockLibSecrets(source => { }); 63 | 64 | act.Should().ThrowExactly().WithMessage("*builder*"); 65 | } 66 | 67 | [Fact] 68 | public static void SetSecretExceptionHandlerMethod() 69 | { 70 | Action onSecretException = context => { }; 71 | 72 | var builder = new ConfigurationBuilder(); 73 | 74 | builder.SetSecretExceptionHandler(onSecretException); 75 | 76 | builder.Properties[ConfigurationBuilderExtensions.SecretExceptionHandlerKey].Should().BeSameAs(onSecretException); 77 | } 78 | 79 | [Fact] 80 | public static void SetSecretExceptionHandlerMethodWithNullBuilder() 81 | { 82 | IConfigurationBuilder builder = null!; 83 | Action onSecretException = context => { }; 84 | 85 | Action act = () => builder.SetSecretExceptionHandler(onSecretException); 86 | 87 | act.Should().ThrowExactly().WithMessage("*builder*"); 88 | } 89 | 90 | [Fact] 91 | public static void SetSecretExceptionHandlerMethodWithNullHandler() 92 | { 93 | var builder = new ConfigurationBuilder(); 94 | 95 | Action act = () => builder.SetSecretExceptionHandler(null!); 96 | 97 | act.Should().ThrowExactly().WithMessage("*onSecretException*"); 98 | } 99 | 100 | [Fact] 101 | public static void GetSecretExceptionHandlerMethod() 102 | { 103 | var builder = new ConfigurationBuilder(); 104 | Action onSecretException = context => { }; 105 | 106 | builder.Properties.Add(ConfigurationBuilderExtensions.SecretExceptionHandlerKey, onSecretException); 107 | 108 | var secretsProvider = builder.GetSecretExceptionHandler(); 109 | 110 | secretsProvider.Should().BeSameAs(onSecretException); 111 | } 112 | 113 | [Fact] 114 | public static void GetSecretExceptionHandlerMethodWithNoHandler() 115 | { 116 | var builder = new ConfigurationBuilder(); 117 | 118 | var onSecretException = builder.GetSecretExceptionHandler(); 119 | 120 | onSecretException.Should().BeNull(); 121 | } 122 | 123 | [Fact] 124 | public static void GetSecretExceptionHandlerMethodWithNullBuilder() 125 | { 126 | IConfigurationBuilder builder = null!; 127 | 128 | Action act = () => builder.GetSecretExceptionHandler(); 129 | 130 | act.Should().ThrowExactly().WithMessage("*builder*"); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Tests/MockSecret.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using System; 3 | 4 | namespace RockLib.Secrets.Tests 5 | { 6 | internal static class MockSecret 7 | { 8 | /// 9 | /// Gets a mock secret that returns the specified value. 10 | /// 11 | public static Mock Get(string key, string value) 12 | { 13 | var mockSecret = new Mock(); 14 | mockSecret.Setup(m => m.ConfigurationKey).Returns(key); 15 | mockSecret.Setup(m => m.GetValue()).Returns(value); 16 | return mockSecret; 17 | } 18 | 19 | /// 20 | /// Gets a mock secret that throws the specified exception. 21 | /// 22 | public static Mock Get(string key, Exception exception) 23 | { 24 | var mockSecret = new Mock(); 25 | mockSecret.Setup(m => m.ConfigurationKey).Returns(key); 26 | mockSecret.Setup(m => m.GetValue()).Throws(exception); 27 | return mockSecret; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Tests/RockLib.Secrets.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | false 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Tests/SecretsConfigurationBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using Xunit; 4 | 5 | namespace RockLib.Secrets.Tests 6 | { 7 | public static class SecretsConfigurationBuilderTests 8 | { 9 | [Fact] 10 | public static void Create() 11 | { 12 | var source = new SecretsConfigurationSource(); 13 | 14 | var builder = new SecretsConfigurationBuilder(source); 15 | 16 | builder.Source.Should().BeSameAs(source); 17 | } 18 | 19 | [Fact] 20 | public static void ConstructorWithNullSource() 21 | { 22 | var act = () => new SecretsConfigurationBuilder(null!); 23 | 24 | act.Should().ThrowExactly().WithMessage("*source*"); 25 | } 26 | 27 | [Fact] 28 | public static void AddSecretMethod() 29 | { 30 | var source = new SecretsConfigurationSource(); 31 | 32 | var builder = new SecretsConfigurationBuilder(source); 33 | 34 | var secret = MockSecret.Get("foo", "bar").Object; 35 | 36 | builder.AddSecret(secret); 37 | 38 | source.Secrets.Should().ContainSingle() 39 | .Which.Should().BeSameAs(secret); 40 | } 41 | 42 | [Fact] 43 | public static void AddSecretMethodWithNullSecret() 44 | { 45 | var source = new SecretsConfigurationSource(); 46 | 47 | var builder = new SecretsConfigurationBuilder(source); 48 | 49 | var act = () => builder.AddSecret(null!); 50 | 51 | act.Should().ThrowExactly().WithMessage("*secret*"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Tests/SecretsConfigurationProviderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using Xunit; 6 | 7 | namespace RockLib.Secrets.Tests 8 | { 9 | public static class SecretsConfigurationProviderTests 10 | { 11 | [Fact] 12 | public static void Create() 13 | { 14 | var secret1 = MockSecret.Get("key1", "value1").Object; 15 | var secret2 = MockSecret.Get("key2", "value2").Object; 16 | 17 | var source = new SecretsConfigurationSource 18 | { 19 | Secrets = { secret1, secret2 } 20 | }; 21 | 22 | using var provider = new SecretsConfigurationProvider(source); 23 | 24 | provider.Source.Should().BeSameAs(source); 25 | 26 | provider.Secrets.Should().NotBeSameAs(source.Secrets); 27 | provider.Secrets.Should().BeEquivalentTo(source.Secrets); 28 | } 29 | 30 | [Fact] 31 | public static void CreateWithNullSource() 32 | { 33 | var act = () => new SecretsConfigurationProvider(null!); 34 | 35 | act.Should().ThrowExactly().WithMessage("*source*"); 36 | } 37 | 38 | [Fact] 39 | public static void CreateWithNoSecrets() 40 | { 41 | var source = new SecretsConfigurationSource(); 42 | 43 | var act = () => new SecretsConfigurationProvider(source); 44 | 45 | act.Should().ThrowExactly().WithMessage("The SecretsConfigurationSource must contain at least one secret.*source*"); 46 | } 47 | 48 | [Fact] 49 | public static void CreateWithNullValueInSecrets() 50 | { 51 | var secret1 = MockSecret.Get("key1", "value1").Object; 52 | ISecret secret2 = null!; 53 | 54 | var source = new SecretsConfigurationSource 55 | { 56 | Secrets = { secret1, secret2 } 57 | }; 58 | 59 | var act = () => new SecretsConfigurationProvider(source); 60 | 61 | act.Should().ThrowExactly().WithMessage("The SecretsConfigurationSource cannot contain any null secrets.*source*"); 62 | } 63 | 64 | [Fact] 65 | public static void CreateWithNullKeyInSecrets() 66 | { 67 | var secret1 = MockSecret.Get("key1", "value1").Object; 68 | var secret2 = MockSecret.Get(null!, "value2").Object; 69 | 70 | var source = new SecretsConfigurationSource 71 | { 72 | Secrets = { secret1, secret2 } 73 | }; 74 | 75 | var act = () => new SecretsConfigurationProvider(source); 76 | 77 | act.Should().ThrowExactly().WithMessage("The SecretsConfigurationSource cannot contain any secrets with a null Key.*source*"); 78 | } 79 | 80 | [Fact] 81 | public static void CreateWithDuplicateKeysInSecrets() 82 | { 83 | var secret1 = MockSecret.Get("key1", "value1").Object; 84 | var secret2 = MockSecret.Get("key1", "value2").Object; 85 | 86 | var source = new SecretsConfigurationSource 87 | { 88 | Secrets = { secret1, secret2 } 89 | }; 90 | 91 | var act = () => new SecretsConfigurationProvider(source); 92 | 93 | act.Should().ThrowExactly().WithMessage("The SecretsConfigurationSource cannot contain any secrets with duplicate Keys.*source*"); 94 | } 95 | 96 | [Fact] 97 | public static void LoadMethod() 98 | { 99 | var secret1 = MockSecret.Get("foo", "abc").Object; 100 | var secret2 = MockSecret.Get("bar", (string)null!).Object; 101 | 102 | var source = new SecretsConfigurationSource 103 | { 104 | Secrets = { secret1, secret2 }, 105 | ReloadMilliseconds = Timeout.Infinite 106 | }; 107 | 108 | using var provider = new SecretsConfigurationProvider(source); 109 | 110 | provider.Load(); 111 | 112 | provider.TryGet("foo", out var fooValue).Should().BeTrue(); 113 | fooValue.Should().Be("abc"); 114 | 115 | provider.TryGet("bar", out var barValue).Should().BeTrue(); 116 | barValue.Should().BeNull(); 117 | } 118 | 119 | [Fact] 120 | public static void LoadMethodWhenGetValueThrowsException() 121 | { 122 | var exception = new ArgumentNullException(); 123 | 124 | var mockSecret1 = MockSecret.Get("foo", "abc"); 125 | var mockSecret2 = MockSecret.Get("bar", exception); 126 | 127 | var caughtExceptions = new List(); 128 | 129 | void OnSecretException(SecretExceptionContext context) 130 | { 131 | caughtExceptions.Add(context.Exception); 132 | } 133 | 134 | var source = new SecretsConfigurationSource 135 | { 136 | Secrets = { mockSecret1.Object, mockSecret2.Object }, 137 | ReloadMilliseconds = Timeout.Infinite, 138 | OnSecretException = OnSecretException 139 | }; 140 | 141 | using var provider = new SecretsConfigurationProvider(source); 142 | 143 | provider.Load(); 144 | 145 | provider.TryGet("foo", out var fooValue).Should().BeTrue(); 146 | fooValue.Should().Be("abc"); 147 | 148 | provider.TryGet("bar", out var barValue).Should().BeTrue(); 149 | barValue.Should().BeNull(); 150 | 151 | caughtExceptions.Should().HaveCount(1); 152 | caughtExceptions[0].Should().BeSameAs(exception); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Tests/RockLib.Secrets.Tests/SecretsConfigurationSourceTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.Extensions.Configuration; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using Xunit; 7 | 8 | namespace RockLib.Secrets.Tests 9 | { 10 | public class SecretsConfigurationSourceTests 11 | { 12 | [Fact] 13 | public void DisableReloadMethod() 14 | { 15 | var source = new SecretsConfigurationSource 16 | { 17 | ReloadMilliseconds = 1000 18 | }; 19 | 20 | source.DisableReload(); 21 | 22 | source.ReloadMilliseconds.Should().Be(Timeout.Infinite); 23 | } 24 | 25 | [Fact] 26 | public void BuildMethod() 27 | { 28 | var secret1 = MockSecret.Get("key1", "value1").Object; 29 | var secret2 = MockSecret.Get("key2", "value2").Object; 30 | 31 | var source = new SecretsConfigurationSource 32 | { 33 | Secrets = { secret1, secret2 } 34 | }; 35 | 36 | var builder = new ConfigurationBuilder() 37 | .AddInMemoryCollection(new Dictionary 38 | { 39 | ["RockLib.Secrets:Type"] = typeof(CustomSecret).AssemblyQualifiedName! 40 | }); 41 | 42 | var provider = (SecretsConfigurationProvider)source.Build(builder); 43 | 44 | provider.Source.Should().BeSameAs(source); 45 | 46 | provider.Secrets.Should().NotBeSameAs(source.Secrets); 47 | provider.Secrets.Should().BeEquivalentTo(source.Secrets); 48 | 49 | provider.Secrets[0].Should().BeSameAs(secret1); 50 | provider.Secrets[1].Should().BeSameAs(secret2); 51 | provider.Secrets[2].Should().BeOfType(); 52 | } 53 | 54 | [Fact] 55 | public void BuildMethodWhenOnSecretExceptionIsNull() 56 | { 57 | Action onSecretException = context => { }; 58 | 59 | var source = new SecretsConfigurationSource 60 | { 61 | Secrets = { MockSecret.Get("key", "value").Object } 62 | }; 63 | 64 | var builder = new ConfigurationBuilder(); 65 | builder.SetSecretExceptionHandler(onSecretException); 66 | 67 | var provider = source.Build(builder); 68 | 69 | var secretsConfigurationProvider = provider.Should().BeOfType().Subject; 70 | secretsConfigurationProvider.Source.Should().BeSameAs(source); 71 | source.OnSecretException.Should().BeSameAs(onSecretException); 72 | } 73 | 74 | [Fact] 75 | public void BuildMethodAddingSecretsOnFirstCall() 76 | { 77 | var secret = MockSecret.Get("key", "value").Object; 78 | 79 | var source = new SecretsConfigurationSource 80 | { 81 | Secrets = { secret } 82 | }; 83 | 84 | var builder = new ConfigurationBuilder() 85 | .AddInMemoryCollection(new Dictionary 86 | { 87 | ["RockLib.Secrets:Type"] = typeof(CustomSecret).AssemblyQualifiedName! 88 | }); 89 | 90 | // Before building, source only contains the single secret that was added directly to it. 91 | source.Secrets.Should().ContainSingle(s => ReferenceEquals(s, secret)); 92 | 93 | source.Build(builder); 94 | 95 | // After building the first time, the secret defined in configuration has been added to the source. 96 | source.Secrets.Should().HaveCount(2); 97 | source.Secrets[0].Should().BeSameAs(secret); 98 | source.Secrets[1].Should().BeOfType(); 99 | 100 | source.Build(builder); 101 | 102 | // After building a second time, source hasn't changed. 103 | source.Secrets.Should().HaveCount(2); 104 | source.Secrets[0].Should().BeSameAs(secret); 105 | source.Secrets[1].Should().BeOfType(); 106 | } 107 | 108 | #pragma warning disable CA1812 109 | private sealed class CustomSecret : ISecret 110 | #pragma warning restore CA1812 111 | { 112 | public string ConfigurationKey => "CustomSecret.Key"; 113 | 114 | public string GetValue() => "CustomSecret.GetValue()"; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /docs/AwsSecret.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | sidebar_label: 'Configure AWS secrets' 4 | --- 5 | 6 | # How to configure AWS secrets 7 | 8 | Add AWS secrets to the `ISecretsConfigurationBuilder` by calling the `AddAwsSecret` method, supplying the following parameters: 9 | - `configurationKey` (required) 10 | - The configuration key for the secret. 11 | - `secretId` (required) 12 | - The Amazon Resource Name (ARN) or the friendly name of the secret. 13 | - `secretKey` (optional) 14 | - The key of the secret in AWS. 15 | - `secretsManager` (optional) 16 | - The client object used for routing calls to AWS. Typically, this is an instance of `AmazonSecretsManagerClient`. 17 | 18 | ```csharp 19 | var builder = new ConfigurationBuilder(); 20 | builder.AddRockLibSecrets() 21 | .AddAwsSecret("MyConnectionString", "MyApp", "ConnectionString", new AmazonSecretsManagerClient()) 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | sidebar_label: 'Define secrets in configuration' 4 | --- 5 | 6 | # How to define secrets in configuration 7 | 8 | Any secrets that are defined in the "RockLib.Secrets" section of the *other* sources of a configuration builder are added to the `SecretsConfigurationSource`. 9 | 10 | For example, if a configuration builder has JSON file and RockLib secrets sources added: 11 | 12 | ```csharp 13 | IConfigurationBuilder builder = new ConfigurationBuilder; 14 | 15 | builder.AddJsonFile("appsettings.json"); 16 | 17 | // Note: No secrets are added directly to the builder in this example, but if 18 | // any had been, they would also be available from the builder's configuration. 19 | builder.AddRockLibSecrets(); 20 | ``` 21 | 22 | And the `appsettings.json` file defines one or more secrets in its "RockLib.Secrets" section, like this: 23 | 24 | ```json 25 | { 26 | "RockLib.Secrets": [ 27 | { 28 | "Type": "ExampleApp.MyCustomSecret, ExampleApp", 29 | "Value": { 30 | "ConfigurationKey": "MyConnectionString" 31 | } 32 | } 33 | ] 34 | } 35 | ``` 36 | 37 | Then when the builder is built, those secrets will be available from the returned `IConfiguration` by the value of their `ConfigurationKey` property. 38 | 39 | ```csharp 40 | IConfiguration config = builder.Build(); 41 | 42 | string myConnectionString = config["MyConnectionString"]; 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/ConfigurationBuilder.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | sidebar_label: 'Add secrets to configuration builder' 4 | --- 5 | 6 | # How to add secrets directly to a configuration builder 7 | 8 | To add secrets to an `IConfigurationBuilder`, call the `AddRockLibSecrets` extension method on the builder, optionally passing an `Action` for [configuring the source](#secretsconfigurationsource). 9 | 10 | ```csharp 11 | IConfigurationBuilder builder = new ConfigurationBuilder(); 12 | 13 | builder.AddRockLibSecrets(source => 14 | { 15 | // TODO: configure source 16 | }); 17 | ``` 18 | 19 | This extension method returns an `ISecretsConfigurationBuilder`, which has a `AddSecret` method for adding secrets. 20 | 21 | ```csharp 22 | IConfigurationBuilder builder = new ConfigurationBuilder(); 23 | 24 | ISecretsConfigurationBuilder secretsBuilder = builder.AddRockLibSecrets(); 25 | 26 | secretsBuilder.AddSecret(new MyCustomSecret(configurationKey: "MyConnectionString")); 27 | ``` 28 | 29 | Implementations of `ISecret`, such as `AwsSecret`, have extension methods for simplifying the adding of a secret: 30 | 31 | ```csharp 32 | IConfigurationBuilder builder = new ConfigurationBuilder(); 33 | 34 | ISecretsConfigurationBuilder secretsBuilder = builder.AddRockLibSecrets(); 35 | 36 | secretsBuilder.AddAwsSecret("MyConnectionString", "MySecretId", "MySecretKey"); 37 | ``` 38 | 39 | --- 40 | 41 | ## SecretsConfigurationSource 42 | 43 | The `SecretsConfigurationSource` defines several options: 44 | 45 | - `Secrets` 46 | - This property defines the secrets that the source provides. 47 | - The `ConfigurationBuilder` class adds to this property when its `AddSecret` method is called. 48 | - `OnSecretException` 49 | - This callback is invoked whenever the `GetValue()` method of an `ISecret` throws an exception. 50 | - `ReloadMilliseconds` 51 | - This property defines how often the configuration provider reloads its data. Specify `Timeout.Infinite` or call the `DisableReload()` method to disable reloading. 52 | -------------------------------------------------------------------------------- /docs/GettingStarted.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Getting Started 6 | 7 | In this tutorial, we will be building a console application that prints out the value of secrets read from configuration, by reversing the secret text. 8 | 9 | --- 10 | 11 | Create a .NET Core console application named "SecretsTutorial". 12 | 13 | --- 14 | 15 | Add a nuget reference for "RockLib.Secrets" to the project. 16 | 17 | --- 18 | 19 | Add a class named 'ReversedSecret.cs' to the project. Replace the default code with the following: 20 | 21 | ```csharp 22 | using RockLib.Secrets; 23 | using System.Linq; 24 | 25 | namespace SecretsTutorial 26 | { 27 | public class ReversedSecret : ISecret 28 | { 29 | private readonly string _secret; 30 | 31 | public ReversedSecret(string configurationKey, string secret) 32 | { 33 | ConfigurationKey = configurationKey; 34 | _secret = secret; 35 | } 36 | 37 | public string ConfigurationKey { get; } 38 | 39 | public string GetValue() 40 | { 41 | return new string(_secret.ToCharArray().Reverse().ToArray()); 42 | } 43 | } 44 | } 45 | ``` 46 | --- 47 | 48 | Add a new JSON file to the project named 'appsettings.json'. Set its 'Copy to Output Directory' setting to 'Copy if newer'. Add the following configuration: 49 | 50 | ```json 51 | { 52 | "FirstSecretKey": "the first secret has not been applied yet", 53 | "SecondSecretKey": "the second secret has not been applied yet", 54 | "ThirdSecretKey": "the third secret has not been applied yet", 55 | 56 | "RockLib.Secrets": [ 57 | { 58 | "Type": "SecretsTutorial.ReversedSecret, SecretsTutorial", 59 | "Value": { 60 | "ConfigurationKey": "FirstSecretKey", 61 | "Secret": "terces tsrif eht si sihT" 62 | } 63 | }, 64 | { 65 | "Type": "SecretsTutorial.ReversedSecret, SecretsTutorial", 66 | "Value": { 67 | "ConfigurationKey": "SecondSecretKey", 68 | "Secret": "terces dnoces eht si sihT" 69 | } 70 | }, 71 | { 72 | "Type": "SecretsTutorial.ReversedSecret, SecretsTutorial", 73 | "Value": { 74 | "ConfigurationKey": "ThirdSecretKey", 75 | "Secret": "terces driht eht si sihT" 76 | } 77 | } 78 | ] 79 | } 80 | ``` 81 | 82 | This configuration will set up three secrets that can be accessed through config via the following keys, "FirstSecretKey", "SecondSecretKey", or "ThirdSecretKey". 83 | 84 | --- 85 | 86 | Edit the `Program.cs` file as follows: 87 | 88 | ```csharp 89 | using Microsoft.Extensions.Configuration; 90 | using RockLib.Secrets; 91 | using System; 92 | using System.Diagnostics; 93 | 94 | namespace SecretsTutorial 95 | { 96 | public class Program 97 | { 98 | public static void Main(string[] args) 99 | { 100 | var configBuilder = new ConfigurationBuilder(); 101 | 102 | configBuilder.AddJsonFile("appsettings.json"); 103 | configBuilder.AddRockLibSecrets(); 104 | 105 | var config = configBuilder.Build(); 106 | 107 | Console.WriteLine("First Secret: " + config["FirstSecretKey"]); 108 | Console.WriteLine("Second Secret: " + config["SecondSecretKey"]); 109 | Console.WriteLine("Third Secret: " + config["ThirdSecretKey"]); 110 | 111 | Debugger.Break(); 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | --- 118 | 119 | Start the application. It should print out the reversed secrets, like below: 120 | 121 | ```powershell 122 | First Secret: This is the first secret 123 | Second Secret: This is the second secret 124 | Third Secret: This is the third secret 125 | ``` 126 | -------------------------------------------------------------------------------- /docs/Usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | sidebar_label: 'Use secrets from RockLib.Secrets' 4 | --- 5 | 6 | # How to use the secrets from RockLib.Secrets 7 | 8 | *There's not much to do.* 9 | 10 | ## From a .NET Core app with DI 11 | 12 | For .NET Core applications that use an `IHostBuilder` or `IWebHostBuilder` and call the `ConfigureAppConfiguration` and `AddRockLibSecrets` extension methods, the `IConfiguration` that is DI registered contains the values of the provider's secrets. Such applications have a Program.cs similar to this: 13 | 14 | ```csharp 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureAppConfiguration(builder => builder.AddRockLibSecrets()); 17 | ``` 18 | 19 | Controllers and servivces can access secret values through configuration like this: 20 | 21 | ```csharp 22 | public class MyService 23 | { 24 | private readonly IConfiguration _config; 25 | 26 | public MyService(IConfiguration config) 27 | { 28 | _config = config; 29 | } 30 | 31 | public void DoSomething() 32 | { 33 | string connectionString = _config["MyConnectionString"]; 34 | } 35 | } 36 | ``` 37 | 38 | ## From a built IConfigurationBuilder 39 | 40 | When building an `IConfiguration` or `IConfigurationRoot` directly, secrets can be accessed by their key: 41 | 42 | ```csharp 43 | IConfigurationBuilder configBuilder = new ConfigurationBuilder(); 44 | 45 | configBuilder.AddRockLibSecrets() 46 | .AddSecret(new MyCustomSecret(configurationKey: "MyConnectionString")); 47 | 48 | IConfiguration config = configBuilder.Build(); 49 | 50 | // Variable will contain whatever MyCustomSecret.GetValue() returned. 51 | string myConnectionString = config["MyConnectionString"]; 52 | ``` 53 | 54 | ## From Config.Root 55 | 56 | The `Config.Root` property from the RockLib.Configuration library automatically adds the secrets defined in its default configuration. See [defining secrets in configuration](Configuration.md) for more details. 57 | 58 | ```json 59 | { 60 | "MyConnectionString": "the connection string secret has not been applied yet", 61 | 62 | "RockLib.Secrets": { 63 | "Type": "ExampleApp.MyCustomSecret, ExampleApp", 64 | "Value": { 65 | "ConfigurationKey": "MyConnectionString" 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | Having the above `appsettings.json` file would allow access to the secrets like this: 72 | 73 | ```csharp 74 | using RockLib.Configuration; 75 | . . . 76 | string myConnectionString = Config.Root["MyConnectionString"]; 77 | ``` 78 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RockLib/RockLib.Secrets/9a920dd0ac2402cf11c55f99187bce8820a2d7cf/icon.png --------------------------------------------------------------------------------