├── .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 | [](https://www.nuget.org/packages/AWSSDK.SecretsManager.Caching)
6 | [](https://github.com/aws/aws-secretsmanager-caching-net/actions/workflows/dotnet.yml)
7 | [](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 | }
--------------------------------------------------------------------------------