├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── dotnet.yml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── SecretsManagerCachingDotNet.sln ├── aws-secretsmanager-caching-net.snk ├── buildspec.yml ├── code-analysis.ruleset ├── src └── Amazon.SecretsManager.Extensions.Caching │ ├── Amazon.SecretsManager.Extensions.Caching.csproj │ ├── ISecretCacheHook.cs │ ├── ISecretsManagerCache.cs │ ├── SecretCacheConfiguration.cs │ ├── SecretCacheItem.cs │ ├── SecretCacheObject.cs │ ├── SecretCacheVersion.cs │ ├── SecretsManagerCache.cs │ └── VersionInfo.cs └── test ├── Amazon.SecretsManager.Extensions.Caching.IntegTests ├── Amazon.SecretsManager.Extensions.Caching.IntegTests.csproj └── IntegrationTests.cs └── Amazon.SecretsManager.Extensions.Caching.UnitTests ├── Amazon.SecretsManager.Extensions.Caching.UnitTests.csproj └── CacheTests.cs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | **To Reproduce** 13 | 14 | 1. 15 | 16 | **Expected behavior** 17 | 18 | **Environment** 19 | 20 | .NET version, OS, etc. 21 | 22 | **Additional context** 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | dependencies: 14 | applies-to: version-updates 15 | dependency-type: production 16 | update-types: 17 | - minor 18 | - patch 19 | - package-ecosystem: "github-actions" # See documentation for possible values 20 | directory: "/" # Location of package manifests 21 | schedule: 22 | interval: "weekly" 23 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install .NET 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: | 20 | 8 21 | - name: Install .NET Framework (add MSBuild to Path) 22 | uses: microsoft/setup-msbuild@v2 23 | with: 24 | msbuild-architecture: x64 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --no-restore 29 | - name: Test 30 | # Strong name requires disabling xUnit app domains in order to get coverage using coverlet 31 | # https://github.com/MarcoRossignoli/coverlet/blob/master/Documentation/KnownIssues.md#tests-fail-if-assembly-is-strong-named 32 | run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage" test/Amazon.SecretsManager.Extensions.Caching.UnitTests -- RunConfiguration.DisableAppDomain=true 33 | - name: Codecov 34 | uses: codecov/codecov-action@v5 35 | with: 36 | directory: test/Amazon.SecretsManager.Extensions.Caching.UnitTests/TestResults 37 | env: 38 | CODECOV_TOKEN: #{{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # Visual Studio per-user settings data 3 | ###################################### 4 | *.suo 5 | *.user 6 | 7 | #################### 8 | # Build/Test folders 9 | #################### 10 | **/.vs/ 11 | **/bin/ 12 | **/obj/ 13 | **/TestResults/ 14 | **/Temp/ 15 | **/NuGet.exe 16 | **/buildlogs/ 17 | **/Deployment/ 18 | **/packages 19 | **/launchSettings.json 20 | 21 | **/node_modules/ 22 | **/TestGenerations/ 23 | 24 | **/.vscode 25 | **/.idea 26 | *.userprefs 27 | **.DS_Store 28 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | README.md @ecraw-amzn 2 | * @aws/aws-secrets-manager-pr-br 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws/aws-secretsmanager-caching-csharp/issues), or [recently closed](https://github.com/aws/aws-secretsmanager-caching-csharp/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/aws-secretsmanager-caching-csharp/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws/aws-secretsmanager-caching-csharp/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Secrets Manager C# Client Side Caching Library 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Secrets Manager Caching Client for .NET 2 | 3 | 4 | 5 | [![NuGet](https://img.shields.io/nuget/v/AWSSDK.SecretsManager.Caching)](https://www.nuget.org/packages/AWSSDK.SecretsManager.Caching) 6 | [![.NET](https://github.com/aws/aws-secretsmanager-caching-net/actions/workflows/dotnet.yml/badge.svg?event=push)](https://github.com/aws/aws-secretsmanager-caching-net/actions/workflows/dotnet.yml) 7 | [![codecov](https://codecov.io/gh/aws/aws-secretsmanager-caching-net/branch/master/graph/badge.svg?token=cugbEh31cw)](https://codecov.io/gh/aws/aws-secretsmanager-caching-net) 8 | 9 | The AWS Secrets Manager caching client enables in-process caching of secrets for .NET applications. 10 | 11 | ## Required Prerequisites 12 | 13 | To use this client, you must have: 14 | 15 | * A .NET project with one of the following: 16 | * .NET Framework 4.6.2 or higher 17 | * .NET Standard 2.0 or higher 18 | 19 | * An Amazon Web Services (AWS) account to access secrets stored in AWS Secrets Manager and use AWS SDK for .NET. 20 | 21 | * **To create an AWS account**, go to [Sign In or Create an AWS Account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) and then choose **I am a new user.** Follow the instructions to create an AWS account. 22 | 23 | * **To create a secret in AWS Secrets Manager**, go to [Creating Secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) and follow the instructions on that page. 24 | 25 | * **To download and install the AWS SDK for .NET**, go to [Installing the AWS SDK for .NET](https://aws.amazon.com/sdk-for-net/) in the AWS SDK for .NET documentation and then follow the instructions on that page. 26 | 27 | ## Download 28 | 29 | You can get the latest release from `Nuget`: 30 | 31 | ```xml 32 | 33 | 34 | 35 | ``` 36 | 37 | ## Getting Started 38 | 39 | The following code sample demonstrates how to start using the caching client: 40 | 41 | ```cs 42 | using System; 43 | using Amazon.SecretsManager.Extensions.Caching.SecretsManagerCache; 44 | 45 | namespace LambdaExample { 46 | public class CachingExample 47 | { 48 | private SecretsManagerCache cache = new SecretsManagerCache(); 49 | private const String MySecretName = "MySecret"; 50 | 51 | public async Task FunctionHandlerAsync(String input, ILambdaContext context) 52 | { 53 | String MySecret = await cache.GetSecretString(MySecretName); 54 | ... 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | * After instantiating the cache, retrieve your secret using `GetSecretString` or `GetSecretBinary`. 61 | * On successive retrievals, the cache will return the cached copy of the secret. 62 | * Learn more about [AWS Lambda Function Handlers in C#](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-programming-model-handler-types.html). 63 | 64 | ### Cache Configuration 65 | 66 | You can configure the `SecretCacheConfiguration` object with the following parameters: 67 | * `CacheItemTTL` - The TTL of a Cache item in milliseconds. The default value is `3600000` ms, or 1 hour. 68 | * `MaxCacheSize` - The maximum number of items the Cache can contain before evicting using LRU. The default value is `1024`. 69 | * `VersionStage` - The Version Stage the Cache will request when retrieving secrets from Secrets Manager. The default value is `AWSCURRENT`. 70 | * `Client` - The Secrets Manager client to be used by the Cache. The default value is `null`, which causes the Cache to instantiate a new Secrets Manager client. 71 | * `CacheHook` - An implementation of the ISecretCacheHook interface. The default value is `null`. 72 | 73 | ## Getting Help 74 | We use GitHub issues for tracking bugs and caching library feature requests and have limited bandwidth to address them. Please use these community resources for getting help: 75 | * Ask a question on [Stack Overflow](https://stackoverflow.com/) and tag it with [aws-secrets-manager](https://stackoverflow.com/questions/tagged/aws-secrets-manager). 76 | * Open a support ticket with [AWS Support](https://console.aws.amazon.com/support/home#/). 77 | * If it turns out that you may have found a bug, please [open an issue](https://github.com/aws/aws-secretsmanager-caching-csharp/issues/new). 78 | 79 | ## License 80 | 81 | This library is licensed under the Apache 2.0 License. 82 | -------------------------------------------------------------------------------- /SecretsManagerCachingDotNet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.539 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{73B3E414-F5D8-4495-A283-D4271126C53A}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C6D686A7-A7EF-4396-9476-70A3A0FB9DEC}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.SecretsManager.Extensions.Caching", "src\Amazon.SecretsManager.Extensions.Caching\Amazon.SecretsManager.Extensions.Caching.csproj", "{B7809806-7B35-4712-8995-30BB71218FE9}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solutionitems", "solutionitems", "{219B6F8C-9706-408E-8584-45045335DAAF}" 13 | ProjectSection(SolutionItems) = preProject 14 | .gitignore = .gitignore 15 | code-analysis.ruleset = code-analysis.ruleset 16 | EndProjectSection 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.SecretsManager.Extensions.Caching.UnitTests", "test\Amazon.SecretsManager.Extensions.Caching.UnitTests\Amazon.SecretsManager.Extensions.Caching.UnitTests.csproj", "{01186448-622D-4725-B21C-54A3FEBCB85F}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.SecretsManager.Extensions.Caching.IntegTests", "test\Amazon.SecretsManager.Extensions.Caching.IntegTests\Amazon.SecretsManager.Extensions.Caching.IntegTests.csproj", "{A07052B9-6D72-47FF-8EFD-6A35C58CF7FC}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {B7809806-7B35-4712-8995-30BB71218FE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {B7809806-7B35-4712-8995-30BB71218FE9}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {B7809806-7B35-4712-8995-30BB71218FE9}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {B7809806-7B35-4712-8995-30BB71218FE9}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {72B442BD-CE77-4EF9-AF26-EBD4FD6B8972}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {72B442BD-CE77-4EF9-AF26-EBD4FD6B8972}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {72B442BD-CE77-4EF9-AF26-EBD4FD6B8972}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {72B442BD-CE77-4EF9-AF26-EBD4FD6B8972}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {01186448-622D-4725-B21C-54A3FEBCB85F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {01186448-622D-4725-B21C-54A3FEBCB85F}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {01186448-622D-4725-B21C-54A3FEBCB85F}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {01186448-622D-4725-B21C-54A3FEBCB85F}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {A07052B9-6D72-47FF-8EFD-6A35C58CF7FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {A07052B9-6D72-47FF-8EFD-6A35C58CF7FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {A07052B9-6D72-47FF-8EFD-6A35C58CF7FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {A07052B9-6D72-47FF-8EFD-6A35C58CF7FC}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {B7809806-7B35-4712-8995-30BB71218FE9} = {73B3E414-F5D8-4495-A283-D4271126C53A} 50 | {01186448-622D-4725-B21C-54A3FEBCB85F} = {C6D686A7-A7EF-4396-9476-70A3A0FB9DEC} 51 | {A07052B9-6D72-47FF-8EFD-6A35C58CF7FC} = {C6D686A7-A7EF-4396-9476-70A3A0FB9DEC} 52 | EndGlobalSection 53 | GlobalSection(ExtensibilityGlobals) = postSolution 54 | SolutionGuid = {1A5339B2-E624-44ED-B714-2F21D7406ED3} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /aws-secretsmanager-caching-net.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-secretsmanager-caching-net/212ac352d1078939cf4160c51e44241fd078441e/aws-secretsmanager-caching-net.snk -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | build: 5 | commands: 6 | - dotnet restore 7 | - dotnet build -c Release --no-restore 8 | - dotnet test -c Release --no-build 9 | - dotnet pack -c Release --no-build 10 | artifacts: 11 | base-directory: 'src/Amazon.SecretsManager.Extensions.Caching/bin' 12 | files: 13 | - 'Release/AWSSDK.SecretsManager.Caching.*.nupkg' 14 | -------------------------------------------------------------------------------- /code-analysis.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/Amazon.SecretsManager.Extensions.Caching.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | ..\..\code-analysis.ruleset 7 | Library 8 | true 9 | AWSSDK.SecretsManager.Caching 10 | 2.0.0 11 | AWS Secrets Manager Caching for .NET 12 | Amazon Web Services 13 | The AWS Secrets Manager .NET caching client enables in-process caching of secrets for .NET applications. 14 | Amazon Web Services 15 | https://github.com/aws/aws-secretsmanager-caching-net 16 | Apache-2.0 17 | https://sdk-for-net.amazonwebservices.com/images/AWSLogo128x128.png 18 | https://github.com/aws/aws-secretsmanager-caching-net 19 | AWS;Amazon;cloud;aws-sdk-v3;secrets;secret manager;secretsmanager;caching;cache 20 | true 21 | ..\..\aws-secretsmanager-caching-net.snk 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/ISecretCacheHook.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | /// 17 | /// Interface to hook the local in-memory cache. This interface will allow 18 | /// for clients to perform actions on the items being stored in the in-memory 19 | /// cache. One example would be encrypting/decrypting items stored in the 20 | /// in-memory cache. 21 | /// 22 | public interface ISecretCacheHook 23 | { 24 | /// 25 | /// Prepare the object for storing in the cache. 26 | /// 27 | object Put(object o); 28 | 29 | /// 30 | /// Derive the object from the cached object. 31 | /// 32 | object Get(object cachedObject); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/ISecretsManagerCache.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | 17 | using System; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | 21 | /// 22 | /// A class used for clide-side caching of secrets stored in AWS Secrets Manager 23 | /// 24 | public interface ISecretsManagerCache : IDisposable 25 | { 26 | 27 | /// 28 | /// Returns the cache entry corresponding to the specified secret if it exists in the cache. 29 | /// Otherwise, the secret value is fetched from Secrets Manager and a new cache entry is created. 30 | /// 31 | SecretCacheItem GetCachedSecret(string secretId); 32 | 33 | /// 34 | /// Asynchronously retrieves the specified SecretBinary after calling . 35 | /// 36 | Task GetSecretBinary(string secretId, CancellationToken cancellationToken = default); 37 | 38 | /// 39 | /// Asynchronously retrieves the specified SecretString after calling . 40 | /// 41 | Task GetSecretString(string secretId, CancellationToken cancellationToken = default); 42 | 43 | /// 44 | /// Requests the secret value from SecretsManager asynchronously and updates the cache entry with any changes. 45 | /// If there is no existing cache entry, a new one is created. 46 | /// Returns true or false depending on if the refresh is successful. 47 | /// 48 | Task RefreshNowAsync(string secretId, CancellationToken cancellationToken = default); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/SecretCacheConfiguration.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | /// 17 | /// A class used for configuring AWS Secrets Manager client-side caching. 18 | /// 19 | public class SecretCacheConfiguration 20 | { 21 | public const ushort DEFAULT_MAX_CACHE_SIZE = 1024; 22 | public const string DEFAULT_VERSION_STAGE = "AWSCURRENT"; 23 | public const uint DEFAULT_CACHE_ITEM_TTL = 3600000; 24 | 25 | /// 26 | /// Gets or sets the TTL of a cache item in milliseconds. The default value for this is 3600000 millseconds, or one hour. 27 | /// 28 | public uint CacheItemTTL { get; set; } = DEFAULT_CACHE_ITEM_TTL; 29 | 30 | /// 31 | /// Gets or sets the maximum number of items the SecretsManagerCache will store before evicting items 32 | /// using the LRU strategy. The default value for this is 1024 items. 33 | /// 34 | public ushort MaxCacheSize { get; set; } = DEFAULT_MAX_CACHE_SIZE; 35 | 36 | /// 37 | /// Gets or sets the Version Stage the SecretsManagerCache will request when retrieving 38 | /// secrets from Secrets Manager. The default value for this is AWSCURRENT. 39 | /// 40 | public string VersionStage { get; set; } = DEFAULT_VERSION_STAGE; 41 | 42 | /// 43 | /// Gets or sets the client implementation. 44 | /// 45 | public IAmazonSecretsManager Client { get; set; } = null; 46 | 47 | /// 48 | /// Gets or sets the optional implementation. 49 | /// 50 | public ISecretCacheHook CacheHook { get; set; } = null; 51 | } 52 | } -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/SecretCacheItem.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Amazon.SecretsManager.Model; 21 | using Microsoft.Extensions.Caching.Memory; 22 | 23 | /// 24 | /// A default class representing a cached secret from AWS Secrets Manager. 25 | /// 26 | public class SecretCacheItem : SecretCacheObject 27 | { 28 | /// The cached secret value versions for this cached secret. 29 | private readonly MemoryCache versions = new MemoryCache(new MemoryCacheOptions()); 30 | private const ushort MAX_VERSIONS_CACHE_SIZE = 10; 31 | 32 | public SecretCacheItem(String secretId, IAmazonSecretsManager client, SecretCacheConfiguration config) 33 | : base(secretId, client, config) 34 | { 35 | } 36 | 37 | /// 38 | /// Asynchronously retrieves the most current DescribeSecretResponse from Secrets Manager 39 | /// as part of the Refresh operation. 40 | /// 41 | protected override async Task ExecuteRefreshAsync(CancellationToken cancellationToken = default) 42 | { 43 | return await client.DescribeSecretAsync(new DescribeSecretRequest { SecretId = secretId }, cancellationToken); 44 | } 45 | 46 | /// 47 | /// Asynchronously retrieves the GetSecretValueResponse from the proper SecretCacheVersion. 48 | /// 49 | protected override async Task GetSecretValueAsync(DescribeSecretResponse result, CancellationToken cancellationToken = default) 50 | { 51 | SecretCacheVersion version = GetVersion(result); 52 | if (version == null) 53 | { 54 | return null; 55 | } 56 | return await version.GetSecretValue(cancellationToken); 57 | } 58 | 59 | public override int GetHashCode() 60 | { 61 | return (secretId ?? string.Empty).GetHashCode(); 62 | } 63 | 64 | public override string ToString() 65 | { 66 | return $"SecretCacheItem: {secretId}"; 67 | } 68 | 69 | public override bool Equals(object obj) 70 | { 71 | return obj is SecretCacheItem sci && string.Equals(this.secretId, sci.secretId); 72 | } 73 | 74 | /// 75 | /// Retrieves the SecretCacheVersion corresponding to the Version Stage 76 | /// specified by the SecretCacheConfiguration. 77 | /// 78 | private SecretCacheVersion GetVersion(DescribeSecretResponse describeResult) 79 | { 80 | if (null == describeResult?.VersionIdsToStages) return null; 81 | String currentVersionId = null; 82 | foreach (KeyValuePair> entry in describeResult.VersionIdsToStages) 83 | { 84 | if (entry.Value.Contains(config.VersionStage)) 85 | { 86 | currentVersionId = entry.Key; 87 | break; 88 | } 89 | } 90 | if (currentVersionId != null) 91 | { 92 | SecretCacheVersion version = versions.Get(currentVersionId); 93 | if (null == version) 94 | { 95 | version = versions.Set(currentVersionId, new SecretCacheVersion(secretId, currentVersionId, client, config)); 96 | if (versions.Count > MAX_VERSIONS_CACHE_SIZE) 97 | { 98 | TrimCacheToSizeLimit(); 99 | } 100 | } 101 | return version; 102 | } 103 | return null; 104 | } 105 | 106 | private void TrimCacheToSizeLimit() 107 | { 108 | versions.Compact((double)(versions.Count - config.MaxCacheSize) / versions.Count); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/SecretCacheObject.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | using Amazon.Runtime; 17 | using Amazon.SecretsManager.Model; 18 | using Amazon.Util; 19 | using System; 20 | using System.Threading; 21 | using System.Threading.Tasks; 22 | 23 | public abstract class SecretCacheObject 24 | { 25 | /// The number of milliseconds to wait after an exception. 26 | private const long EXCEPTION_BACKOFF = 1000; 27 | 28 | /// The growth factor of the backoff duration. 29 | private const long EXCEPTION_BACKOFF_GROWTH_FACTOR = 2; 30 | 31 | /// The maximum number of milliseconds to wait before retrying a failed 32 | /// request. 33 | private const long BACKOFF_PLATEAU = 128 * EXCEPTION_BACKOFF; 34 | 35 | private JitteredDelay EXCEPTION_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF), 36 | TimeSpan.FromMilliseconds(EXCEPTION_BACKOFF), 37 | TimeSpan.FromMilliseconds(BACKOFF_PLATEAU)); 38 | 39 | /// When forcing a refresh using the refreshNow method, a random sleep 40 | /// will be performed using this value. This helps prevent code from 41 | /// executing a refreshNow in a continuous loop without waiting. 42 | private const long FORCE_REFRESH_JITTER_BASE_INCREMENT = 3500; 43 | private const long FORCE_REFRESH_JITTER_VARIANCE = 1000; 44 | 45 | private JitteredDelay FORCE_REFRESH_JITTERED_DELAY = new JitteredDelay(TimeSpan.FromMilliseconds(FORCE_REFRESH_JITTER_BASE_INCREMENT), 46 | TimeSpan.FromMilliseconds(FORCE_REFRESH_JITTER_VARIANCE)); 47 | 48 | /// The secret identifier for this cached object. 49 | protected String secretId; 50 | 51 | /// A private object to synchronize access to certain methods. 52 | protected static readonly SemaphoreSlim Lock = new SemaphoreSlim(1,1); 53 | 54 | /// The AWS Secrets Manager client to use for requesting secrets. 55 | protected IAmazonSecretsManager client; 56 | 57 | /// The Secret Cache Configuration. 58 | protected SecretCacheConfiguration config; 59 | 60 | /// A flag to indicate a refresh is needed. 61 | private bool refreshNeeded = true; 62 | 63 | /// The result of the last AWS Secrets Manager request for this item. 64 | private Object data = null; 65 | 66 | /// If the last request to AWS Secrets Manager resulted in an exception, 67 | /// that exception will be thrown back to the caller when requesting 68 | /// secret data. 69 | protected Exception exception = null; 70 | 71 | 72 | /// The number of exceptions encountered since the last successfully 73 | /// AWS Secrets Manager request. This is used to calculate an exponential 74 | /// backoff. 75 | private long exceptionCount = 0; 76 | 77 | /// The time to wait before retrying a failed AWS Secrets Manager request. 78 | private long nextRetryTime = 0; 79 | 80 | public static readonly ThreadLocal random = new ThreadLocal(() => new Random(Environment.TickCount)); 81 | 82 | 83 | 84 | /// 85 | /// Construct a new cached item for the secret. 86 | /// 87 | /// The secret identifier. This identifier could be the full ARN or the friendly name for the secret. 88 | /// The AWS Secrets Manager client to use for requesting the secret. 89 | /// The secret cache configuration. 90 | public SecretCacheObject(String secretId, IAmazonSecretsManager client, SecretCacheConfiguration config) 91 | { 92 | this.secretId = secretId; 93 | this.client = client; 94 | this.config = config; 95 | } 96 | 97 | protected abstract Task ExecuteRefreshAsync(CancellationToken cancellationToken = default); 98 | 99 | protected abstract Task GetSecretValueAsync(T result, CancellationToken cancellationToken = default); 100 | 101 | /// 102 | /// Return the typed result object. 103 | /// 104 | private T GetResult() 105 | { 106 | if (null != config.CacheHook) 107 | { 108 | return (T)config.CacheHook.Get(data); 109 | } 110 | return (T)data; 111 | } 112 | 113 | /// 114 | /// Store the result data. 115 | /// 116 | private void SetResult(T result) 117 | { 118 | if (null != config.CacheHook) 119 | { 120 | data = config.CacheHook.Put(result); 121 | } 122 | else 123 | { 124 | data = result; 125 | } 126 | } 127 | 128 | /// 129 | /// Determine if the secret object should be refreshed. 130 | /// 131 | protected bool IsRefreshNeeded() 132 | { 133 | if (refreshNeeded) { return true; } 134 | if (null == exception) { return false; } 135 | 136 | // If we encountered an exception on the last attempt 137 | // we do not want to keep retrying without a pause between 138 | // the refresh attempts. 139 | // 140 | // If we have exceeded our backoff time we will refresh 141 | // the secret now. 142 | return Environment.TickCount >= nextRetryTime; 143 | } 144 | 145 | /// 146 | /// Refresh the cached secret state only when needed. 147 | /// 148 | private async Task RefreshAsync(CancellationToken cancellationToken = default) 149 | { 150 | if (!IsRefreshNeeded()) { return false; } 151 | refreshNeeded = false; 152 | try 153 | { 154 | SetResult(await ExecuteRefreshAsync(cancellationToken)); 155 | exception = null; 156 | exceptionCount = 0; 157 | return true; 158 | } 159 | catch (Exception ex) when (ex is AmazonServiceException || ex is AmazonClientException) 160 | { 161 | exception = ex; 162 | // Determine the amount of growth in exception backoff time based on the growth 163 | // factor and default backoff duration. 164 | 165 | nextRetryTime = Environment.TickCount + EXCEPTION_JITTERED_DELAY.GetRetryDelay((int)exceptionCount).Milliseconds; 166 | } 167 | return false; 168 | } 169 | 170 | /// 171 | /// Method to force the refresh of a cached secret state. 172 | /// Returns true if the refresh completed without error. 173 | /// 174 | public async Task RefreshNowAsync(CancellationToken cancellationToken = default) 175 | { 176 | refreshNeeded = true; 177 | // When forcing a refresh, always sleep with a random jitter 178 | // to prevent coding errors that could be calling refreshNow 179 | // in a loop. 180 | long sleep = FORCE_REFRESH_JITTERED_DELAY.GetRetryDelay(1).Milliseconds; 181 | 182 | // Make sure we are not waiting for the next refresh after an 183 | // exception. If we are, sleep based on the retry delay of 184 | // the refresh to prevent a hard loop in attempting to refresh a 185 | // secret that continues to throw an exception such as AccessDenied. 186 | if (null != exception) 187 | { 188 | long wait = nextRetryTime - Environment.TickCount; 189 | sleep = Math.Max(wait, sleep); 190 | } 191 | Thread.Sleep((int)sleep); 192 | 193 | // Perform the requested refresh. 194 | bool success = false; 195 | await Lock.WaitAsync(cancellationToken); 196 | try 197 | { 198 | success = await RefreshAsync(cancellationToken); 199 | } 200 | finally 201 | { 202 | Lock.Release(); 203 | } 204 | return (null == exception && success); 205 | } 206 | 207 | /// 208 | /// Asynchronously return the cached result from AWS Secrets Manager for GetSecretValue. 209 | /// If the secret is due for a refresh, the refresh will occur before the result is returned. 210 | /// If the refresh fails, the cached result is returned, or the cached exception is thrown. 211 | /// 212 | public async Task GetSecretValue(CancellationToken cancellationToken) 213 | { 214 | bool success = false; 215 | await Lock.WaitAsync(cancellationToken); 216 | try 217 | { 218 | success = await RefreshAsync(cancellationToken); 219 | } 220 | finally 221 | { 222 | Lock.Release(); 223 | } 224 | 225 | if (!success && null == data && null != exception) 226 | { 227 | System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception).Throw(); 228 | } 229 | return await GetSecretValueAsync(GetResult(), cancellationToken); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/SecretCacheVersion.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | using Amazon.SecretsManager.Model; 17 | using System; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | 21 | class SecretCacheVersion : SecretCacheObject 22 | { 23 | private readonly String versionId; 24 | private readonly int hash; 25 | 26 | public SecretCacheVersion(String secretId, String versionId, IAmazonSecretsManager client, SecretCacheConfiguration config) 27 | : base(secretId, client, config) 28 | { 29 | this.versionId = versionId; 30 | this.hash = $"{secretId} {versionId}".GetHashCode(); 31 | } 32 | 33 | public override bool Equals(object obj) 34 | { 35 | return obj is SecretCacheVersion scv 36 | && this.secretId == scv.secretId 37 | && this.versionId == scv.versionId; 38 | } 39 | 40 | public override int GetHashCode() 41 | { 42 | return this.hash; 43 | } 44 | 45 | public override string ToString() 46 | { 47 | return $"SecretCacheVersion: {secretId} {versionId}"; 48 | } 49 | 50 | /// 51 | /// Asynchronously retrieves the most current GetSecretValueResponse from Secrets Manager 52 | /// as part of the Refresh operation. 53 | /// 54 | protected override async Task ExecuteRefreshAsync(CancellationToken cancellationToken = default) 55 | { 56 | return await this.client.GetSecretValueAsync(new GetSecretValueRequest { SecretId = this.secretId, VersionId = this.versionId }, cancellationToken); 57 | } 58 | 59 | protected override Task GetSecretValueAsync(GetSecretValueResponse result, CancellationToken cancellationToken = default) 60 | { 61 | return Task.FromResult(result); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/SecretsManagerCache.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | using System; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | using Amazon.Runtime; 20 | using Amazon.SecretsManager.Model; 21 | using Microsoft.Extensions.Caching.Memory; 22 | 23 | /// 24 | /// A class used for clide-side caching of secrets stored in AWS Secrets Manager 25 | /// 26 | public class SecretsManagerCache : ISecretsManagerCache 27 | { 28 | private readonly IAmazonSecretsManager secretsManager; 29 | private readonly SecretCacheConfiguration config; 30 | private readonly MemoryCacheEntryOptions cacheItemPolicy; 31 | private readonly MemoryCache cache = new MemoryCache(new MemoryCacheOptions{ CompactionPercentage = 0 }); 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// 36 | public SecretsManagerCache() 37 | : this(new AmazonSecretsManagerClient(), new SecretCacheConfiguration()) 38 | { 39 | } 40 | 41 | /// 42 | /// Initializes a new instance of the class. 43 | /// 44 | public SecretsManagerCache(IAmazonSecretsManager secretsManager) 45 | : this(secretsManager, new SecretCacheConfiguration()) 46 | { 47 | } 48 | 49 | /// 50 | /// Initializes a new instance of the class. 51 | /// 52 | public SecretsManagerCache(SecretCacheConfiguration config) 53 | : this(new AmazonSecretsManagerClient(), config) 54 | { 55 | } 56 | 57 | /// 58 | /// Initializes a new instance of the class. 59 | /// 60 | public SecretsManagerCache(IAmazonSecretsManager secretsManager, SecretCacheConfiguration config) 61 | { 62 | this.config = config; 63 | this.secretsManager = secretsManager; 64 | cacheItemPolicy = new MemoryCacheEntryOptions() 65 | { 66 | AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(this.config.CacheItemTTL) 67 | }; 68 | if (this.secretsManager is AmazonSecretsManagerClient sm) 69 | { 70 | sm.BeforeRequestEvent += this.ServiceClientBeforeRequestEvent; 71 | } 72 | } 73 | 74 | private void ServiceClientBeforeRequestEvent(object sender, RequestEventArgs e) 75 | { 76 | if (e is WebServiceRequestEventArgs args && args.Headers.ContainsKey(VersionInfo.USER_AGENT_HEADER) && !args.Headers[VersionInfo.USER_AGENT_HEADER].Contains(VersionInfo.USER_AGENT_STRING)) 77 | args.Headers[VersionInfo.USER_AGENT_HEADER] = String.Format("{0}/{1}", args.Headers[VersionInfo.USER_AGENT_HEADER], VersionInfo.USER_AGENT_STRING); 78 | } 79 | 80 | /// 81 | /// Disposes all resources currently being used by the SecretManagerCache's underlying MemoryCache. 82 | /// 83 | public void Dispose() 84 | { 85 | cache.Dispose(); 86 | } 87 | 88 | /// 89 | /// Asynchronously retrieves the specified SecretString after calling . 90 | /// 91 | public async Task GetSecretString(String secretId, CancellationToken cancellationToken = default) 92 | { 93 | SecretCacheItem secret = GetCachedSecret(secretId); 94 | GetSecretValueResponse response = null; 95 | response = await secret.GetSecretValue(cancellationToken); 96 | return response?.SecretString; 97 | } 98 | 99 | /// 100 | /// Asynchronously retrieves the specified SecretBinary after calling . 101 | /// 102 | public async Task GetSecretBinary(String secretId, CancellationToken cancellationToken = default) 103 | { 104 | SecretCacheItem secret = GetCachedSecret(secretId); 105 | GetSecretValueResponse response = null; 106 | response = await secret.GetSecretValue(cancellationToken); 107 | return response?.SecretBinary?.ToArray(); 108 | } 109 | 110 | /// 111 | /// Requests the secret value from SecretsManager asynchronously and updates the cache entry with any changes. 112 | /// If there is no existing cache entry, a new one is created. 113 | /// Returns true or false depending on if the refresh is successful. 114 | /// 115 | public async Task RefreshNowAsync(String secretId, CancellationToken cancellationToken = default) 116 | { 117 | return await GetCachedSecret(secretId).RefreshNowAsync(cancellationToken); 118 | } 119 | 120 | /// 121 | /// Returns the cache entry corresponding to the specified secret if it exists in the cache. 122 | /// Otherwise, the secret value is fetched from Secrets Manager and a new cache entry is created. 123 | /// 124 | public SecretCacheItem GetCachedSecret(string secretId) 125 | { 126 | SecretCacheItem secret = cache.Get(secretId); 127 | if (secret == null) 128 | { 129 | secret = cache.Set(secretId, new SecretCacheItem(secretId, secretsManager, config), cacheItemPolicy); 130 | if (cache.Count > config.MaxCacheSize) 131 | { 132 | // Trim cache size to MaxCacheSize, evicting entries using LRU. 133 | cache.Compact((double)(cache.Count - config.MaxCacheSize) / cache.Count); 134 | } 135 | } 136 | 137 | return secret; 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /src/Amazon.SecretsManager.Extensions.Caching/VersionInfo.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching 15 | { 16 | internal static class VersionInfo 17 | { 18 | /// 19 | /// Incremented for design changes that break backward compatibility. 20 | /// 21 | public const string VERSION_NUM = "1"; 22 | 23 | /// 24 | /// Incremented for major changes to the implementation 25 | /// 26 | public const string MAJOR_REVISION_NUM = "1"; 27 | 28 | /// 29 | /// Incremented for minor changes to the implementation 30 | /// 31 | public const string MINOR_REVISION_NUM = "0"; 32 | 33 | /// 34 | /// Incremented for releases containing an immediate bug fix. 35 | /// 36 | public const string BUGFIX_REVISION_NUM = "0"; 37 | 38 | /// 39 | /// The value used as the user agent header name. 40 | /// 41 | public const string USER_AGENT_HEADER = "User-Agent"; 42 | 43 | /// 44 | /// The release version string. 45 | /// 46 | public static readonly string RELEASE_VERSION = $"{VERSION_NUM}.{MAJOR_REVISION_NUM}.{MINOR_REVISION_NUM}.{BUGFIX_REVISION_NUM}"; 47 | 48 | /// 49 | /// The user agent string that will be appended to the SDK user agent string 50 | /// 51 | public static readonly string USER_AGENT_STRING = $"AwsSecretCache/{RELEASE_VERSION}"; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/Amazon.SecretsManager.Extensions.Caching.IntegTests/Amazon.SecretsManager.Extensions.Caching.IntegTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net48 5 | false 6 | true 7 | true 8 | ..\..\aws-secretsmanager-caching-net.snk 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/Amazon.SecretsManager.Extensions.Caching.IntegTests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | namespace Amazon.SecretsManager.Extensions.Caching.IntegTests 2 | { 3 | using Xunit; 4 | using Amazon.SecretsManager.Model; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | 12 | // Performs test secret cleanup before and after integ tests are run 13 | public class TestBase : IAsyncLifetime 14 | { 15 | public static IAmazonSecretsManager Client = new AmazonSecretsManagerClient(Amazon.RegionEndpoint.USWest2); 16 | public static String TestSecretPrefix = "IntegTest"; 17 | public static List SecretNamesToDelete = new List(); 18 | 19 | public async Task InitializeAsync() 20 | { 21 | await FindPreviousTestSecrets(); 22 | await DeleteSecrets(forceDelete: false); 23 | } 24 | 25 | public async Task DisposeAsync() 26 | { 27 | await DeleteSecrets(forceDelete: true); 28 | } 29 | 30 | private async Task FindPreviousTestSecrets() 31 | { 32 | String nextToken = null; 33 | var twoDaysAgo = DateTime.Now.AddDays(-2); 34 | do 35 | { 36 | var response = await TestBase.Client.ListSecretsAsync(new ListSecretsRequest { NextToken = nextToken }); 37 | nextToken = response.NextToken; 38 | List secretList = response.SecretList; 39 | foreach (SecretListEntry secret in secretList) 40 | { 41 | if (secret.Name.StartsWith(TestSecretPrefix) 42 | && DateTime.Compare(secret.LastChangedDate ?? throw new InvalidOperationException("Value for LastChangedDate is null."), twoDaysAgo) < 0 43 | && DateTime.Compare(secret.LastAccessedDate ?? throw new InvalidOperationException("Value for LastAccessedDate is null."), twoDaysAgo) < 0) 44 | { 45 | SecretNamesToDelete.Add(secret.Name); 46 | } 47 | } 48 | } while (nextToken != null); 49 | } 50 | 51 | private async Task DeleteSecrets(bool forceDelete) 52 | { 53 | foreach (String secretName in SecretNamesToDelete) 54 | { 55 | await TestBase.Client.DeleteSecretAsync(new DeleteSecretRequest { SecretId = secretName, ForceDeleteWithoutRecovery = forceDelete }); 56 | } 57 | SecretNamesToDelete.Clear(); 58 | } 59 | } 60 | 61 | public class IntegrationTests : IClassFixture 62 | { 63 | private SecretsManagerCache cache; 64 | private String testSecretString = System.Guid.NewGuid().ToString(); 65 | private MemoryStream testSecretBinary = new MemoryStream(Enumerable.Repeat((byte)0x20, 10).ToArray()); 66 | 67 | private enum TestType { SecretString = 0, SecretBinary = 1 }; 68 | 69 | private async Task Setup(TestType type) 70 | { 71 | String testSecretName = TestBase.TestSecretPrefix + Guid.NewGuid().ToString(); 72 | CreateSecretRequest req = null; 73 | 74 | if (type == TestType.SecretString) 75 | { 76 | req = new CreateSecretRequest { Name = testSecretName, SecretString = testSecretString }; 77 | } 78 | else if (type == TestType.SecretBinary) 79 | { 80 | req = new CreateSecretRequest { Name = testSecretName, SecretBinary = testSecretBinary }; 81 | } 82 | 83 | await TestBase.Client.CreateSecretAsync(req); 84 | TestBase.SecretNamesToDelete.Add(testSecretName); 85 | return testSecretName; 86 | } 87 | 88 | [Fact] 89 | public async Task GetSecretStringTest() 90 | { 91 | String testSecretName = await Setup(TestType.SecretString); 92 | cache = new SecretsManagerCache(TestBase.Client); 93 | Assert.Equal(await cache.GetSecretString(testSecretName), testSecretString); 94 | } 95 | 96 | [Fact] 97 | public async Task SecretCacheTTLTest() 98 | { 99 | String testSecretName = await Setup(TestType.SecretString); 100 | cache = new SecretsManagerCache(TestBase.Client, new SecretCacheConfiguration { CacheItemTTL = 1000 }); 101 | String originalSecretString = await cache.GetSecretString(testSecretName); 102 | await TestBase.Client.UpdateSecretAsync(new UpdateSecretRequest { SecretId = testSecretName, SecretString = System.Guid.NewGuid().ToString() }); 103 | 104 | // Even though the secret is updated, the cached version should be retrieved 105 | Assert.Equal(originalSecretString, await cache.GetSecretString(testSecretName)); 106 | 107 | Thread.Sleep(1000); 108 | 109 | // Cached secret string should be expired and the updated secret string retrieved 110 | Assert.NotEqual(originalSecretString, await cache.GetSecretString(testSecretName)); 111 | } 112 | 113 | [Fact] 114 | public async Task SecretCacheRefreshTest() 115 | { 116 | String testSecretName = await Setup(TestType.SecretString); 117 | cache = new SecretsManagerCache(TestBase.Client); 118 | String originalSecretString = await cache.GetSecretString(testSecretName); 119 | await TestBase.Client.UpdateSecretAsync(new UpdateSecretRequest { SecretId = testSecretName, SecretString = System.Guid.NewGuid().ToString() }); 120 | 121 | Assert.Equal(originalSecretString, await cache.GetSecretString(testSecretName)); 122 | Assert.True(await cache.RefreshNowAsync(testSecretName)); 123 | Assert.NotEqual(originalSecretString, await cache.GetSecretString(testSecretName)); 124 | } 125 | 126 | [Fact] 127 | public async Task NoSecretBinaryTest() 128 | { 129 | String testSecretName = await Setup(TestType.SecretString); 130 | cache = new SecretsManagerCache(TestBase.Client); 131 | Assert.Null(await cache.GetSecretBinary(testSecretName)); 132 | } 133 | 134 | [Fact] 135 | public async Task GetSecretBinaryTest() 136 | { 137 | String testSecretName = await Setup(TestType.SecretBinary); 138 | cache = new SecretsManagerCache(TestBase.Client); 139 | Assert.Equal(await cache.GetSecretBinary(testSecretName), testSecretBinary.ToArray()); 140 | } 141 | 142 | [Fact] 143 | public async Task NoSecretStringTest() 144 | { 145 | String testSecretName = await Setup(TestType.SecretBinary); 146 | cache = new SecretsManagerCache(TestBase.Client); 147 | Assert.Null(await cache.GetSecretString(testSecretName)); 148 | } 149 | 150 | [Fact] 151 | public async Task CacheHookTest() 152 | { 153 | String testSecretName = await Setup(TestType.SecretString); 154 | TestHook testHook = new TestHook(); 155 | cache = new SecretsManagerCache(TestBase.Client, new SecretCacheConfiguration { CacheHook = testHook }); 156 | String originalSecretString = await cache.GetSecretString(testSecretName); 157 | } 158 | 159 | class TestHook : ISecretCacheHook 160 | { 161 | private Dictionary dictionary = new Dictionary(); 162 | public object Get(object cachedObject) 163 | { 164 | return dictionary[(int)cachedObject]; 165 | } 166 | 167 | public object Put(object o) 168 | { 169 | int key = dictionary.Count; 170 | dictionary.Add(key, o); 171 | return key; 172 | } 173 | 174 | public int GetCount() 175 | { 176 | return dictionary.Count; 177 | } 178 | } 179 | } 180 | } 181 | 182 | -------------------------------------------------------------------------------- /test/Amazon.SecretsManager.Extensions.Caching.UnitTests/Amazon.SecretsManager.Extensions.Caching.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net48 5 | false 6 | true 7 | true 8 | ..\..\aws-secretsmanager-caching-net.snk 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/Amazon.SecretsManager.Extensions.Caching.UnitTests/CacheTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://aws.amazon.com/apache2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | namespace Amazon.SecretsManager.Extensions.Caching.UnitTests 15 | { 16 | using System; 17 | using System.Collections.Generic; 18 | using System.IO; 19 | using System.Linq; 20 | using System.Threading; 21 | using System.Threading.Tasks; 22 | using Amazon.Runtime; 23 | using Amazon.SecretsManager.Model; 24 | using Moq; 25 | using Xunit; 26 | 27 | public class CacheTests 28 | { 29 | private const string AWSCURRENT_VERSIONID_1 = "01234567890123456789012345678901"; 30 | private const string AWSCURRENT_VERSIONID_2 = "12345678901234567890123456789012"; 31 | 32 | private readonly GetSecretValueResponse secretStringResponse1 = new GetSecretValueResponse 33 | { 34 | Name = "MySecretString", 35 | VersionId = AWSCURRENT_VERSIONID_1, 36 | SecretString = "MySecretValue1", 37 | }; 38 | 39 | private readonly GetSecretValueResponse secretStringResponse2 = new GetSecretValueResponse 40 | { 41 | Name = "MySecretString", 42 | VersionId = AWSCURRENT_VERSIONID_2, 43 | SecretString = "MySecretValue2" 44 | }; 45 | 46 | private readonly GetSecretValueResponse secretStringResponse3 = new GetSecretValueResponse 47 | { 48 | Name = "OtherSecretString", 49 | VersionId = AWSCURRENT_VERSIONID_1, 50 | SecretString = "MyOtherSecretValue" 51 | }; 52 | 53 | private readonly GetSecretValueResponse secretStringResponse4 = new GetSecretValueResponse 54 | { 55 | Name = "AnotherSecretString", 56 | VersionId = AWSCURRENT_VERSIONID_1, 57 | SecretString = "AnotherSecretValue" 58 | }; 59 | 60 | private readonly GetSecretValueResponse binaryResponse1 = new GetSecretValueResponse 61 | { 62 | Name = "MyBinarySecret", 63 | VersionId = AWSCURRENT_VERSIONID_1, 64 | SecretBinary = new MemoryStream(Enumerable.Repeat((byte)0x20, 10).ToArray()) 65 | }; 66 | 67 | private readonly GetSecretValueResponse binaryResponse2 = new GetSecretValueResponse 68 | { 69 | Name = "MyBinarySecret", 70 | VersionId = AWSCURRENT_VERSIONID_2, 71 | SecretBinary = new MemoryStream(Enumerable.Repeat((byte)0x30, 10).ToArray()) 72 | }; 73 | 74 | private readonly DescribeSecretResponse describeSecretResponse1 = new DescribeSecretResponse() 75 | { 76 | VersionIdsToStages = new Dictionary> { 77 | { AWSCURRENT_VERSIONID_1, new List { "AWSCURRENT" } } 78 | } 79 | }; 80 | 81 | private readonly DescribeSecretResponse describeSecretResponse2 = new DescribeSecretResponse() 82 | { 83 | VersionIdsToStages = new Dictionary> { 84 | { AWSCURRENT_VERSIONID_2, new List { "AWSCURRENT" } } 85 | } 86 | }; 87 | 88 | 89 | [Fact] 90 | public void SecretCacheConstructorTest() 91 | { 92 | Mock secretsManager = new Mock(MockBehavior.Strict); 93 | SecretsManagerCache cache1 = new SecretsManagerCache(secretsManager.Object); 94 | SecretsManagerCache cache2 = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration()); 95 | Assert.NotNull(cache1); 96 | Assert.NotNull(cache2); 97 | } 98 | 99 | [Fact] 100 | public async Task GetSecretStringTest() 101 | { 102 | Mock secretsManager = new Mock(MockBehavior.Strict); 103 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 104 | .ReturnsAsync(secretStringResponse1) 105 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 106 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 107 | .ReturnsAsync(describeSecretResponse1) 108 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 109 | 110 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 111 | string first = await cache.GetSecretString(secretStringResponse1.Name); 112 | Assert.Equal(first, secretStringResponse1.SecretString); 113 | } 114 | 115 | [Fact] 116 | public async Task NoSecretStringPresentTest() 117 | { 118 | Mock secretsManager = new Mock(MockBehavior.Strict); 119 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 120 | .ReturnsAsync(binaryResponse1) 121 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 122 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 123 | .ReturnsAsync(describeSecretResponse1) 124 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 125 | 126 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 127 | string first = await cache.GetSecretString(secretStringResponse1.Name); 128 | Assert.Null(first); 129 | } 130 | 131 | [Fact] 132 | public async Task GetSecretBinaryTest() 133 | { 134 | Mock secretsManager = new Mock(MockBehavior.Strict); 135 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 136 | .ReturnsAsync(binaryResponse1) 137 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 138 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 139 | .ReturnsAsync(describeSecretResponse1) 140 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 141 | 142 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 143 | 144 | byte[] first = await cache.GetSecretBinary(binaryResponse1.Name); 145 | Assert.Equal(first, binaryResponse1.SecretBinary.ToArray()); 146 | } 147 | 148 | [Fact] 149 | public async Task NoSecretBinaryPresentTest() 150 | { 151 | Mock secretsManager = new Mock(MockBehavior.Strict); 152 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 153 | .ReturnsAsync(secretStringResponse1) 154 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 155 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 156 | .ReturnsAsync(describeSecretResponse1) 157 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 158 | 159 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 160 | 161 | byte[] first = await cache.GetSecretBinary(binaryResponse1.Name); 162 | Assert.Null(first); 163 | } 164 | 165 | [Fact] 166 | public async Task GetSecretBinaryMultipleTest() 167 | { 168 | Mock secretsManager = new Mock(MockBehavior.Strict); 169 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 170 | .ReturnsAsync(binaryResponse1) 171 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 172 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 173 | .ReturnsAsync(describeSecretResponse1) 174 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 175 | 176 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 177 | 178 | byte[] first = null; 179 | for (int i = 0; i < 10; i++) 180 | { 181 | first = await cache.GetSecretBinary(binaryResponse1.Name); 182 | } 183 | Assert.Equal(first, binaryResponse1.SecretBinary.ToArray()); 184 | 185 | } 186 | 187 | [Fact] 188 | public async Task BasicSecretCacheTest() 189 | { 190 | Mock secretsManager = new Mock(MockBehavior.Strict); 191 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 192 | .ReturnsAsync(secretStringResponse1) 193 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 194 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 195 | .ReturnsAsync(describeSecretResponse1) 196 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 197 | 198 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 199 | 200 | String first = await cache.GetSecretString(secretStringResponse1.Name); 201 | String second = await cache.GetSecretString(secretStringResponse1.Name); 202 | Assert.Equal(first, second); 203 | 204 | } 205 | 206 | [Fact] 207 | public async Task SecretStringRefreshNowTest() 208 | { 209 | Mock secretsManager = new Mock(MockBehavior.Strict); 210 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 211 | .ReturnsAsync(secretStringResponse1) 212 | .ReturnsAsync(secretStringResponse2) 213 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 214 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 215 | .ReturnsAsync(describeSecretResponse1) 216 | .ReturnsAsync(describeSecretResponse2) 217 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 218 | 219 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 220 | { 221 | String first = await cache.GetSecretString(secretStringResponse1.Name); 222 | bool success = await cache.RefreshNowAsync(secretStringResponse1.Name); 223 | String second = await cache.GetSecretString(secretStringResponse1.Name); 224 | Assert.True(success); 225 | Assert.NotEqual(first, second); 226 | } 227 | } 228 | 229 | [Fact] 230 | public async Task BinarySecretRefreshNowTest() 231 | { 232 | Mock secretsManager = new Mock(MockBehavior.Strict); 233 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 234 | .ReturnsAsync(binaryResponse1) 235 | .ReturnsAsync(binaryResponse2) 236 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 237 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == binaryResponse1.Name), default(CancellationToken))) 238 | .ReturnsAsync(describeSecretResponse1) 239 | .ReturnsAsync(describeSecretResponse2) 240 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 241 | 242 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 243 | byte[] first = await cache.GetSecretBinary(binaryResponse1.Name); 244 | bool success = await cache.RefreshNowAsync(binaryResponse1.Name); 245 | byte[] second = await cache.GetSecretBinary(binaryResponse1.Name); 246 | Assert.True(success); 247 | Assert.NotEqual(first, second); 248 | } 249 | 250 | [Fact] 251 | public async Task RefreshNowFailedTest() 252 | { 253 | Mock secretsManager = new Mock(MockBehavior.Strict); 254 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 255 | .ReturnsAsync(secretStringResponse1) 256 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 257 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 258 | .ReturnsAsync(describeSecretResponse1) 259 | .ThrowsAsync(new AmazonServiceException("Caught exception")); 260 | 261 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 262 | String first = await cache.GetSecretString(secretStringResponse1.Name); 263 | bool success = await cache.RefreshNowAsync(secretStringResponse1.Name); 264 | String second = await cache.GetSecretString(secretStringResponse2.Name); 265 | Assert.False(success); 266 | Assert.Equal(first, second); 267 | } 268 | 269 | [Fact] 270 | public async Task BasicSecretCacheTTLRefreshTest() 271 | { 272 | Mock secretsManager = new Mock(MockBehavior.Strict); 273 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 274 | .ReturnsAsync(secretStringResponse1) 275 | .ReturnsAsync(secretStringResponse2) 276 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 277 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 278 | .ReturnsAsync(describeSecretResponse1) 279 | .ReturnsAsync(describeSecretResponse2) 280 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 281 | 282 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration { CacheItemTTL = 1000 }); 283 | 284 | String first = await cache.GetSecretString(secretStringResponse1.Name); 285 | String second = await cache.GetSecretString(secretStringResponse1.Name); 286 | Assert.Equal(first, second); 287 | 288 | Thread.Sleep(5000); 289 | String third = await cache.GetSecretString(secretStringResponse2.Name); 290 | Assert.NotEqual(second, third); 291 | } 292 | 293 | [Fact] 294 | public async Task GetSecretStringMultipleTest() 295 | { 296 | Mock secretsManager = new Mock(MockBehavior.Strict); 297 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 298 | .ReturnsAsync(secretStringResponse1) 299 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 300 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 301 | .ReturnsAsync(describeSecretResponse2) 302 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 303 | 304 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 305 | String first = null; 306 | for (int i = 0; i < 10; i++) 307 | { 308 | first = await cache.GetSecretString(secretStringResponse1.Name); 309 | } 310 | Assert.Equal(first, secretStringResponse1.SecretString); 311 | } 312 | 313 | [Fact] 314 | public async Task TestBasicCacheEviction() 315 | { 316 | Mock secretsManager = new Mock(MockBehavior.Strict); 317 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 318 | .ReturnsAsync(secretStringResponse1) 319 | .ReturnsAsync(secretStringResponse2) 320 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 321 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse3.Name), default(CancellationToken))) 322 | .ReturnsAsync(secretStringResponse3) 323 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 324 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.IsAny(), default(CancellationToken))) 325 | .ReturnsAsync(describeSecretResponse1) 326 | .ReturnsAsync(describeSecretResponse1) 327 | .ReturnsAsync(describeSecretResponse2) 328 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 329 | 330 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration { MaxCacheSize = 1 }); 331 | String first = await cache.GetSecretString(secretStringResponse1.Name); 332 | String second = await cache.GetSecretString(secretStringResponse3.Name); 333 | String third = await cache.GetSecretString(secretStringResponse2.Name); 334 | Assert.NotEqual(first, third); 335 | } 336 | 337 | [Fact] 338 | public async Task TestBasicErrorCaching() 339 | { 340 | Mock secretsManager = new Mock(MockBehavior.Strict); 341 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 342 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 343 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.Is(j => j.SecretId == secretStringResponse1.Name), default(CancellationToken))) 344 | .ThrowsAsync(new AmazonServiceException("Expected exception")) 345 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 346 | 347 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 348 | for (int i = 0; i < 5; i++) 349 | { 350 | try 351 | { 352 | await cache.GetSecretString(secretStringResponse1.Name); 353 | } 354 | catch (AmazonSecretsManagerException) 355 | { 356 | throw; 357 | } 358 | catch (AmazonServiceException) 359 | { 360 | } 361 | } 362 | 363 | return; 364 | } 365 | 366 | [Fact] 367 | public async Task ExceptionRetryTest() 368 | { 369 | Mock secretsManager = new Mock(MockBehavior.Strict); 370 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.IsAny(), default(CancellationToken))) 371 | .ThrowsAsync(new AmazonServiceException("Expected exception 1")) 372 | .ThrowsAsync(new AmazonServiceException("Expected exception 2")) 373 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 374 | 375 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object); 376 | int retryCount = 10; 377 | 378 | for (int i = 0; i < retryCount; i++) 379 | { 380 | try 381 | { 382 | await cache.GetSecretString(""); 383 | } 384 | catch (AmazonServiceException exception) 385 | { 386 | Assert.Equal("Expected exception 1", exception.Message); 387 | } 388 | 389 | } 390 | 391 | // Wait for backoff interval before retrying to verify a retry is performed. 392 | Thread.Sleep(2100); 393 | 394 | try 395 | { 396 | await cache.GetSecretString(""); 397 | } 398 | catch (AmazonServiceException exception) 399 | { 400 | Assert.Equal("Expected exception 2", exception.Message); 401 | } 402 | } 403 | 404 | class TestHook : ISecretCacheHook 405 | { 406 | private Dictionary dictionary = new Dictionary(); 407 | public object Get(object cachedObject) 408 | { 409 | return dictionary[(int)cachedObject]; 410 | } 411 | 412 | public object Put(object o) 413 | { 414 | int key = dictionary.Count; 415 | dictionary.Add(key, o); 416 | return key; 417 | } 418 | 419 | public int GetCount() 420 | { 421 | return dictionary.Count; 422 | } 423 | } 424 | 425 | [Fact] 426 | public async Task HookSecretCacheTest() 427 | { 428 | Mock secretsManager = new Mock(MockBehavior.Strict); 429 | secretsManager.SetupSequence(i => i.GetSecretValueAsync(It.IsAny(), default(CancellationToken))) 430 | .ReturnsAsync(secretStringResponse1) 431 | .ReturnsAsync(binaryResponse1) 432 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 433 | secretsManager.SetupSequence(i => i.DescribeSecretAsync(It.IsAny(), default(CancellationToken))) 434 | .ReturnsAsync(describeSecretResponse1) 435 | .ReturnsAsync(describeSecretResponse1) 436 | .ThrowsAsync(new AmazonSecretsManagerException("This should not be called")); 437 | 438 | TestHook testHook = new TestHook(); 439 | SecretsManagerCache cache = new SecretsManagerCache(secretsManager.Object, new SecretCacheConfiguration { CacheHook = testHook }); 440 | 441 | for (int i = 0; i < 10; i++) 442 | { 443 | Assert.Equal(await cache.GetSecretString(secretStringResponse1.Name), secretStringResponse1.SecretString); 444 | } 445 | Assert.Equal(2, testHook.GetCount()); 446 | 447 | for (int i = 0; i < 10; i++) 448 | { 449 | Assert.Equal(await cache.GetSecretBinary(binaryResponse1.Name), binaryResponse1.SecretBinary.ToArray()); 450 | } 451 | Assert.Equal(4, testHook.GetCount()); 452 | } 453 | } 454 | } --------------------------------------------------------------------------------