diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..429c768 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** + +**To Reproduce** +Steps to reproduce: +1. + +**Expected behavior** + +**Environment** + +Platform, Rust Version, etc. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..809b7b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,10 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ab40d21 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +*Issue #, if available:* + +*Description of changes:* + + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..98e44ee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..7abb692 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,25 @@ +name: Rust + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Lint + run: cargo fmt --all -- --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c335994 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +debug/ +target/ +**/*.rs.bk +*.pdb +logs/ + +####### + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +######### + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..94a2665 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,64 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'aws_secretsmanager_agent'", + "cargo": { + "args": [ + "build", + "--bin=aws_secretsmanager_agent", + "--package=aws_secretsmanager_agent" + ], + "filter": { + "name": "aws_secretsmanager_agent", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'aws_secretsmanager_agent'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=aws_secretsmanager_agent", + "--package=aws_secretsmanager_agent" + ], + "filter": { + "name": "aws_secretsmanager_agent", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'aws_secretsmanager_caching'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=aws_secretsmanager_caching" + ], + "filter": { + "name": "aws_secretsmanager_caching", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5b627cf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c4b6a1c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,59 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *main* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +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. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +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' issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +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. + + +## Licensing + +See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ff58942 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2703 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "assert-json-diff" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4259cbe96513d2f1073027a259fc2ca917feb3026a5a8d984e3628e490255cc0" +dependencies = [ + "extend", + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "aws-config" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf6cfe2881cb1fcbba9ae946fb9a6480d3b7a714ca84c74925014a89ef3387a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 0.2.12", + "hyper 0.14.29", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-runtime" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c5f920ffd1e0526ec9e70e50bf444db50b204395a0fa7016bbf9e31ea1698f" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-secretsmanager" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74011f98573dd2c499092ece401b93dbdd35de342c404a3cd7de34c282f877e8" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcfae7bf8b8f14cade7579ffa8956fcee91dc23633671096b4b5de7d16f682a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b30def8f02ba81276d5dbc22e7bf3bed20d62d1b175eef82680d6bdc7a6f4c" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0804f840ad31537d5d1a4ec48d59de5e674ad05f1db7d3def2c9acadaf1f7e60" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.1.0", + "once_cell", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-mocks-experimental" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1069164f54cd37cdcf67e30f77ed996ccd71ad85344b9bb0412a1ca224617b" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-protocol-test" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31e8279cb24640c7349f2bda6ca818d5fcc85129386bd73c1d0999430d6ddf2" +dependencies = [ + "assert-json-diff", + "aws-smithy-runtime-api", + "http 0.2.12", + "pretty_assertions", + "regex-lite", + "roxmltree", + "serde_json", + "thiserror", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4217d39fe940066174e6238310167bf466bfbebf3be0661e53cacccde6313" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-protocol-test", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.0", + "httparse", + "hyper 0.14.29", + "hyper-rustls", + "indexmap 2.2.6", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30819352ed0a04ecf6a2f3477e344d2d1ba33d43e0f09ad9047c12e0d923616f" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.1.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "http-body 1.0.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "aws_secretsmanager_agent" +version = "1.0.0" +dependencies = [ + "aws-config", + "aws-sdk-secretsmanager", + "aws-sdk-sts", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws_secretsmanager_caching", + "bytes", + "config", + "http 0.2.12", + "http-body-util", + "hyper 1.4.0", + "hyper-util", + "log", + "log4rs", + "pretty_env_logger", + "serde", + "serde_derive", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "aws_secretsmanager_caching" +version = "1.0.0" +dependencies = [ + "aws-sdk-secretsmanager", + "aws-smithy-mocks-experimental", + "aws-smithy-runtime", + "aws-smithy-types", + "http 0.2.12", + "linked-hash-map", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.70", +] + +[[package]] +name = "darling_macro" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "extend" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47da3a72ec598d9c8937a7ebca8962a5c7a1f28444e38c2b33c771ba3f55f05" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.29", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.4.0", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "serde", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derivative", + "flate2", + "fnv", + "humantime", + "libc", + "log", + "log-mdc", + "once_cell", + "parking_lot", + "rand", + "serde", + "serde-value", + "serde_json", + "serde_yaml", + "thiserror", + "thread-id", + "typemap-ors", + "winapi", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "pest_meta" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "indexmap 2.2.6", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_with" +version = "3.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.2.6", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "thread-id" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.70", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typemap-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +dependencies = [ + "unsafe-any-ors", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unsafe-any-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +dependencies = [ + "destructure_traitobject", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "uuid" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.70", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.70", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +dependencies = [ + "memchr", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..08a14b1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +resolver = "2" + +members = ["aws_secretsmanager_agent", "aws_secretsmanager_caching"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..616fc58 --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b17507d --- /dev/null +++ b/README.md @@ -0,0 +1,335 @@ +# AWS Secrets Manager Agent + +The AWS Secrets Manager Agent is a client\-side HTTP service that you can use to standardize consumption of secrets from Secrets Manager across environments such as AWS Lambda, Amazon Elastic Container Service, Amazon Elastic Kubernetes Service, and Amazon Elastic Compute Cloud\. The Secrets Manager Agent can retrieve and cache secrets in memory so that your applications can consume secrets directly from the cache\. That means you can fetch the secrets your application needs from the localhost instead of making calls to Secrets Manager\. The Secrets Manager Agent can only make read requests to Secrets Manager \- it can't modify secrets\. + +The Secrets Manager Agent uses the AWS credentials you provide in your environment to make calls to Secrets Manager\. The Secrets Manager Agent offers protection against Server Side Request Forgery \(SSRF\) to help improve secret security\. You can configure the Secrets Manager Agent by setting the maximum number of connections, the time to live \(TTL\), the localhost HTTP port, and the cache size\. + +Because the Secrets Manager Agent uses an in\-memory cache, it resets when the Secrets Manager Agent restarts\. The Secrets Manager Agent periodically refreshes the cached secret value\. The refresh happens when you try to read a secret from the Secrets Manager Agent after the TTL has expired\. The default refresh frequency \(TTL\) is 300 seconds, and you can change it by using a [Configuration file](#secrets-manager-agent-config) which you pass to the Secrets Manager Agent using the `--config` command line argument\. The Secrets Manager Agent does not include cache invalidation\. For example, if a secret rotates before the cache entry expires, the Secrets Manager Agent might return a stale secret value\. + +The Secrets Manager Agent returns secret values in the same format as the response of `GetSecretValue`\. Secret values are not encrypted in the cache\. + +To download the source code, see [https://github\.com/aws/aws\-secretsmanager\-agent](https://github.com/aws/aws-secretsmanager-agent) on GitHub\. + +**Topics** +- [AWS Secrets Manager Agent](#aws-secrets-manager-agent) + - [Step 1: Build the Secrets Manager Agent binary](#step-1-build-the-secrets-manager-agent-binary) + - [\[ RPM-based systems \]](#-rpm-based-systems-) + - [\[ Debian-based systems \]](#-debian-based-systems-) + - [\[ Windows \]](#-windows-) + - [\[ Cross-compile natively \]](#-cross-compile-natively-) + - [\[ Cross compile with Rust cross \]](#-cross-compile-with-rust-cross-) + - [Step 2: Install the Secrets Manager Agent](#step-2-install-the-secrets-manager-agent) + - [\[ Amazon EKS and Amazon ECS \]](#-amazon-eks-and-amazon-ecs-) + - [\[ Docker \]](#-docker-) + - [\[ AWS Lambda \]](#-aws-lambda-) + - [Step 3: Retrieve secrets with the Secrets Manager Agent](#step-3-retrieve-secrets-with-the-secrets-manager-agent) + - [\[ curl \]](#-curl-) + - [\[ Python \]](#-python-) + - [Configure the Secrets Manager Agent](#configure-the-secrets-manager-agent) + - [Logging](#logging) + - [Security considerations](#security-considerations) + +## Step 1: Build the Secrets Manager Agent binary + +To build the Secrets Manager Agent binary natively, you need the standard development tools and the Rust tools\. Alternatively, you can cross\-compile for systems that support it, or you can use Rust cross to cross\-compile\. + +------ +#### [ RPM\-based systems ] + +1. On RPM\-based systems such as AL2023, you can install the development tools by using the Development Tools group\. + + ```sh + sudo yum -y groupinstall "Development Tools" + ``` + +1. Follow the instructions at [Install Rust](https://www.rust-lang.org/tools/install) in the *Rust documentation*\. + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + . "$HOME/.cargo/env" + ``` + +1. Build the agent using the cargo build command: + + ```sh + cargo build --release + ``` + + You will find the executable under `target/release/aws-secrets-manager-agent`\. + +------ +#### [ Debian\-based systems ] + +1. On Debian\-based systems such as Ubuntu, you can install the developer tools using the build\-essential package\. + + ```sh + sudo apt install build-essential + ``` + +1. Follow the instructions at [Install Rust](https://www.rust-lang.org/tools/install) in the *Rust documentation*\. + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + . "$HOME/.cargo/env" + ``` + +1. Build the agent using the cargo build command: + + ```sh + cargo build --release + ``` + + You will find the executable under `target/release/aws-secrets-manager-agent`\. + +------ +#### [ Windows ] + +To build on Windows, follow the instructions at [Set up your dev environment on Windows for Rust](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup) in the *Microsoft Windows documentation*\. + +------ +#### [ Cross\-compile natively ] + +On distributions where the mingw\-w64 package is available such as Ubuntu, you can cross compile natively\. + +```sh +# Install the cross compile tool chain +sudo add-apt-repository universe +sudo apt install -y mingw-w64 + +# Install the rust build targets +rustup target add x86_64-pc-windows-gnu + +# Cross compile the agent for Windows +cargo build --release --target x86_64-pc-windows-gnu +``` + +You will find the executable at `target/x86_64-pc-windows-gnu/release/aws-secrets-manager-agent.exe`\. + +------ +#### [ Cross compile with Rust cross ] + +If the cross compile tools are not available natively on the system, you can use the Rust cross project\. For more information, see [https://github\.com/cross\-rs/cross](https://github.com/cross-rs/cross)\. + +**Important** +We recommend 32GB disk space for the build environment\. + +```sh +# Install and start docker +sudo yum -y install docker +sudo systemctl start docker +sudo systemctl enable docker # Make docker start after reboot + +# Give ourselves permission to run the docker images without sudo +sudo usermod -aG docker $USER +newgrp docker + +# Install cross and cross compile the executable +cargo install cross +cross build --release --target x86_64-pc-windows-gnu +``` + +------ + +## Step 2: Install the Secrets Manager Agent + +Based on the type of compute, you have several options for installing the Secrets Manager Agent\. + +------ +#### [ Amazon EKS and Amazon ECS ] + +**To install the Secrets Manager Agent** + +1. Use the `install` script provided in the repository\. + + The script generates a random SSRF token on startup and stores it in the file `/var/run/awssmatoken`\. The token is readable by the `awssmatokenreader` group that the install script creates\. + +1. To allow your application to read the token file, you need to add the user account that your application runs under to the `awssmatokenreader` group\. For example, you can grant permissions for your application to read the token file with the following usermod command, where ** is the user ID under which your application runs\. + + ```sh + sudo usermod -aG awssmatokenreader + ``` + +------ +#### [ Docker ] + +You can run the Secrets Manager Agent as a sidecar container alongside your application by using Docker\. Then your application can retrieve secrets from the local HTTP server the Secrets Manager Agent provides\. For information about Docker, see the [Docker documentation](https://docs.docker.com)\. + +**To create a sidecar container for the Secrets Manager Agent with Docker** + +1. Create a Dockerfile for the Secrets Manager Agent sidecar container\. The following example creates a Docker container with the Secrets Manager Agent binary\. + + ```dockerfile + # Use the latest Debian image as the base + FROM debian:latest + + # Set the working directory inside the container + WORKDIR /app + + # Copy the Secrets Manager Agent binary to the container + COPY secrets-manager-agent . + + # Install any necessary dependencies + RUN apt-get update && apt-get install -y ca-certificates + + # Set the entry point to run the Secrets Manager Agent binary + ENTRYPOINT ["./secrets-manager-agent"] + ``` + +1. Create a Dockerfile for your client application\. + +1. Create a Docker Compose file to run both containers, being sure that they use the same network interface\. This is necessary because the Secrets Manager Agent does not accept requests from outside the localhost interface\. The following example shows a Docker Compose file where the `network_mode` key attaches the `secrets-manager-agent` container to the network namespace of the `client-application` container, which allows them to share the same network interface\. +**Important** +You must load AWS credentials and the SSRF token for the application to be able to use the Secrets Manager Agent\. See the following: +[Manage access](https://docs.aws.amazon.com/eks/latest/userguide/cluster-auth.html) in the *Amazon Elastic Kubernetes Service User Guide* +[Amazon ECS task IAM role](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) in the *Amazon Elastic Container Service Developer Guide* + + ```yaml + version: '3' + services: + client-application: + container_name: client-application + build: + context: . + dockerfile: Dockerfile.client + command: tail -f /dev/null # Keep the container running + + + secrets-manager-agent: + container_name: secrets-manager-agent + build: + context: . + dockerfile: Dockerfile.agent + network_mode: "container:client-application" # Attach to the client-application container's network + depends_on: + - client-application + ``` + +1. Copy the `secrets-manager-agent` binary to the same directory that contains your Dockerfiles and Docker Compose file\. + +1. Build and run the containers based on the provided Dockerfiles by using the following [https://docs.docker.com/reference/cli/docker/compose/](https://docs.docker.com/reference/cli/docker/compose/) command\. + + ```sh + docker-compose up --build + ``` + +1. In your client container, you can now use the Secrets Manager Agent to retrieve secrets\. For more information, see [Step 3: Retrieve secrets with the Secrets Manager Agent](#secrets-manager-agent-call)\. + +------ +#### [ AWS Lambda ] + +You can package the Secrets Manager Agent as an AWS Lambda extension\. Then you can add it to your Lambda function as a layer and call the Secrets Manager Agent from your Lambda function to get secrets\. For an example script that shows how to run the Secrets Manager Agent as an extension, see `secrets-manager-agent-extension.sh` in [https://github\.com/aws/aws\-secretsmanager\-agent](https://github.com/aws/aws-secretsmanager-agent)\. + +**To create a Lambda extension that packages the Secrets Manager Agent** + +1. Create a ZIP file with the Secrets Manager Agent binary\. For instructions, see [Packaging your layer content](https://docs.aws.amazon.com/lambda/latest/dg/packaging-layers.html) in the *AWS Lambda Developer Guide*\. + +1. Create a Lambda layer from the ZIP file\. For instructions, see [Creating layers](https://docs.aws.amazon.com/lambda/latest/dg/creating-deleting-layers.html) in the *AWS Lambda Developer Guide*\. + +1. Add the layer to your Lambda function\. For instructions, see [Adding layers to functions](https://docs.aws.amazon.com/lambda/latest/dg/adding-layers.html) in the *AWS Lambda Developer Guide*\. + +1. In your Lambda function code, you can now use the Secrets Manager Agent to retrieve secrets\. For more information, see [Step 3: Retrieve secrets with the Secrets Manager Agent](#secrets-manager-agent-call)\. + +------ + +## Step 3: Retrieve secrets with the Secrets Manager Agent + +To use the agent, you call the local Secrets Manager Agent endpoint and include the name or ARN of the secret as a query parameter\. By default, the Secrets Manager Agent retrieves the `AWSCURRENT` version of the secret\. To retrieve a different version, you can set `versionStage` or `versionId`\. + +To help protect the Secrets Manager Agent, you must include a SSRF token header as part of each request: `X-Aws-Parameters-Secrets-Token`\. The Secrets Manager Agent denies requests that don't have this header or that have an invalid SSRF token\. You can customize the SSRF header name in the [Configuration file](#secrets-manager-agent-config)\. + +The Secrets Manager Agent uses the AWS SDK for Rust, which uses the [https://docs.aws.amazon.com/sdk-for-rust/latest/dg/credentials.html](https://docs.aws.amazon.com/sdk-for-rust/latest/dg/credentials.html)\. The identity of these IAM credentials determines the permissions the Secrets Manager Agent has to retrieve secrets\. + +**Required permissions: ** ++ `secretsmanager:DescribeSecret` ++ `secretsmanager:GetSecretValue` + +For more information, see [Permissions reference](reference_iam-permissions.md)\. + +**Important** +After the secret value is pulled into the Secrets Manager Agent, any user with access to the compute environment and SSRF token can access the secret from the Secrets Manager Agent cache\. For more information, see [Security considerations](#secrets-manager-agent-security)\. + +------ +#### [ curl ] + +The following curl example shows how to get a secret from the Secrets Manager Agent\. The example relies on the SSRF being present in a file, which is where it is stored by the install script\. + +```sh +curl -v -H \ + "X-Aws-Parameters-Secrets-Token: $(}'; \ + echo +``` + +------ +#### [ Python ] + +The following Python example shows how to get a secret from the Secrets Manager Agent\. The example relies on the SSRF being present in a file, which is where it is stored by the install script\. + +```python +import requests +import json + +# Function that fetches the secret from Secrets Manager Agent for the provided secret id. +def get_secret(): + # Construct the URL for the GET request + url = f"http://localhost:2773/secretsmanager/get?secretId=}" + + # Get the SSRF token from the token file + with open('/var/run/awssmatoken') as fp: + token = fp.read() + + headers = { + "X-Aws-Parameters-Secrets-Token": token.strip() + } + + try: + # Send the GET request with headers + response = requests.get(url, headers=headers) + + # Check if the request was successful + if response.status_code == 200: + # Return the secret value + return response.text + else: + # Handle error cases + raise Exception(f"Status code {response.status_code} - {response.text}") + + except Exception as e: + # Handle network errors + raise Exception(f"Error: {e}") +``` + +------ + +## Configure the Secrets Manager Agent + +To change the configuration of the Secrets Manager Agent, create a [TOML](https://toml.io/en/) config file, and then call `./aws-secrets-manager-agent --config config.toml`\. + +The following list shows the options you can configure for the Secrets Manager Agent\. ++ **log\_level** – The level of detail reported in logs for the Secrets Manager Agent: DEBUG, INFO, WARN, ERROR, or NONE\. The default is INFO\. ++ **http\_port** – The port for the local HTTP server, in the range 1024 to 65535\. The default is 2773\. ++ **region** – The AWS Region to use for requests\. If no Region is specified, the Secrets Manager Agent determines the Region from the SDK\. For more information, see [Specify your credentials and default Region](https://docs.aws.amazon.com/sdk-for-rust/latest/dg/credentials.html) in the *AWS SDK for Rust Developer Guide*\. ++ **ttl\_seconds** – The TTL in seconds for the cached items, in the range 1 to 3600\. The default is 300\. This setting is not used if the cache size is 0\. ++ **cache\_size** – The maximum number of secrets that can be stored in the cache, in the range 0 to 1000\. 0 indicates that there is no caching\. The default is 1000\. ++ **ssrf\_headers** – A list of header names the Secrets Manager Agent checks for the SSRF token\. The default is "X\-Aws\-Parameters\-Secrets\-Token, X\-Vault\-Token"\. ++ **ssrf\_env\_variables** – A list of environment variable names the Secrets Manager Agent checks for the SSRF token\. The environment variable can contain the token or a reference to the token file as in: `AWS_TOKEN=file:///var/run/awssmatoken`\. The default is "AWS\_TOKEN, AWS\_SESSION\_TOKEN"\. ++ **path\_prefix** – The URI prefix used to determine if the request is a path based request\. The default is "/v1/"\. ++ **max\_conn** – The maximum number of connections from HTTP clients that the Secrets Manager Agent allows, in the range 1 to 1000\. The default is 800\. + +## Logging + +The Secrets Manager Agent logs errors locally to the file `logs/secrets_manager_agent.log`\. When your application calls the Secrets Manager Agent to get a secret, those calls appear in the local log\. They do not appear in the CloudTrail logs\. + +The Secrets Manager Agent creates a new log file when the file reaches 10 MB, and it stores up to five log files total\. + +The log does not go to Secrets Manager, CloudTrail, or CloudWatch\. Requests to get secrets from the Secrets Manager Agent do not appear in those logs\. When the Secrets Manager Agent makes a call to Secrets Manager to get a secret, that call is recorded in CloudTrail with a user agent string containing `aws-secrets-manager-agent`\. + +You can configure logging in the [Configuration file](#secrets-manager-agent-config)\. + +## Security considerations + +The Secrets Manager Agent provides compatibility for legacy applications that access secrets through an existing agent or that need caching for langages not supported through other solutions\. + +For an agent architecture, the domain of trust is where the agent endpoint and SSRF token are accessible, which is usually the entire host\. The domain of trust for the Secrets Manager Agent should match the domain where the Secrets Manager credentials are available in order to maintain the same security posture\. For example, on Amazon EC2 the domain of trust for the Secrets Manager Agent would be the same as the domain of the credentials when using roles for Amazon EC2\. + +Security conscious applications that are not already using an agent solution with the Secrets Manager credentials locked down to the application should consider using the language\-specific AWS SDKs or caching solutions\. For more information, see [Get secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets.html)\. \ No newline at end of file diff --git a/aws_secretsmanager_agent/.gitignore b/aws_secretsmanager_agent/.gitignore new file mode 100644 index 0000000..d255700 --- /dev/null +++ b/aws_secretsmanager_agent/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +logs diff --git a/aws_secretsmanager_agent/Cargo.toml b/aws_secretsmanager_agent/Cargo.toml new file mode 100644 index 0000000..4fed636 --- /dev/null +++ b/aws_secretsmanager_agent/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "aws_secretsmanager_agent" +version = "1.0.0" +edition = "2021" + +[dependencies] +hyper = { version = "1", features = ["http1", "server"] } +tokio = { version = "1", features = ["rt-multi-thread", "net", "macros"] } +http-body-util = "0.1" +hyper-util = { version = "0.1", features = ["tokio"]} +bytes = "1" + +pretty_env_logger = "0.5" +serde = "1" +serde_json = "1" +serde_derive = "1" +config = "0.14" + +aws-config = "1" +aws-sdk-secretsmanager = "1" +aws-smithy-runtime-api = "1" +aws-sdk-sts = "1" +log = "0.4.20" +log4rs = { version = "1.2.0", features = ["gzip"] } +url = "2" +aws_secretsmanager_caching = { version = "1.0.0", path = "../aws_secretsmanager_caching" } + +# For unit tests +[dev-dependencies] +hyper = { version = "1", features = ["http1", "server", "client"]} +aws-smithy-runtime = { version = "1", features = ["test-util"] } +tokio = {version = "1", features = ["test-util", "rt-multi-thread", "net", "macros"] } +http = "0.2.9" +aws-smithy-types = "1" diff --git a/aws_secretsmanager_agent/configuration/awssmaseedtoken b/aws_secretsmanager_agent/configuration/awssmaseedtoken new file mode 100755 index 0000000..f535467 --- /dev/null +++ b/aws_secretsmanager_agent/configuration/awssmaseedtoken @@ -0,0 +1,23 @@ +#!/bin/bash -e + +PATH=/bin:/usr/bin:/sbin:/usr/sbin +TOKENFILE=/var/run/awssmatoken +TOKENGROUP=awssmatokenreader +case "$1" in + start) + echo -e "Initializing SMA SSRF token" + touch ${TOKENFILE} + chgrp ${TOKENGROUP} ${TOKENFILE} + chmod 640 ${TOKENFILE} + dd if=/dev/urandom bs=32 count=1 2>/dev/null \ + | sha256sum -b \ + | cut -f1 -d' ' \ + > ${TOKENFILE} + ;; + stop) + echo -e "Clearing SMA SSRF token" + rm -f ${TOKENFILE} + ;; +esac + +exit 0 diff --git a/aws_secretsmanager_agent/configuration/awssmaseedtoken.service b/aws_secretsmanager_agent/configuration/awssmaseedtoken.service new file mode 100644 index 0000000..b6ac0f4 --- /dev/null +++ b/aws_secretsmanager_agent/configuration/awssmaseedtoken.service @@ -0,0 +1,15 @@ +[Unit] +Description=Initialize the SSRF token for AWS Secrets Manager Agent +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +Restart=no +TimeoutSec=1min +ExecStart=/opt/aws/secretsmanageragent/bin/awssmaseedtoken start +ExecStop=/opt/aws/secretsmanageragent/bin/awssmaseedtoken stop + +[Install] +WantedBy=multi-user.target diff --git a/aws_secretsmanager_agent/configuration/awssmastartup.service b/aws_secretsmanager_agent/configuration/awssmastartup.service new file mode 100644 index 0000000..66f2673 --- /dev/null +++ b/aws_secretsmanager_agent/configuration/awssmastartup.service @@ -0,0 +1,19 @@ + +[Unit] +Description=Run the AWS Secrets Manager Agent +After=awssmaseedtoken.service +Requires=awssmaseedtoken.service +After=network-online.target +Wants=network-online.target + +[Service] +User=awssmauser +WorkingDirectory=/opt/aws/secretsmanageragent +Environment="AWS_TOKEN=file:///var/run/awssmatoken" +Type=exec +Restart=always +TimeoutSec=1min +ExecStart=/opt/aws/secretsmanageragent/bin/aws-secrets-manager-agent + +[Install] +WantedBy=multi-user.target diff --git a/aws_secretsmanager_agent/configuration/install b/aws_secretsmanager_agent/configuration/install new file mode 100755 index 0000000..d8a2fbe --- /dev/null +++ b/aws_secretsmanager_agent/configuration/install @@ -0,0 +1,44 @@ +#!/bin/bash -e + +PATH=/bin:/usr/bin:/sbin:/usr/sbin # Use a safe path + +AGENTDIR=/opt/aws/secretsmanageragent +AGENTBIN=aws-secrets-manager-agent +TOKENGROUP=awssmatokenreader +AGENTUSER=awssmauser +TOKENSCRIPT=awssmaseedtoken +AGENTSCRIPT=awssmastartup + +SYSTEMDFILES=/etc/systemd/system + +if [ `id -u` -ne 0 ]; then + echo "This script must be run as root" >&2 + exit 1 +fi + +if [ ! -r ${TOKENSCRIPT} ]; then + echo "Can not read ${TOKENSCRIPT}" >&2 + exit 1 +fi + +if [ ! -r ${AGENTBIN} ]; then + echo "Can not read ${AGENTBIN}" >&2 + exit 1 +fi + +groupadd -f ${TOKENGROUP} +useradd -r -m -g ${TOKENGROUP} -d ${AGENTDIR} ${AGENTUSER} +chmod 755 ${AGENTDIR} + +install -D -T -m 755 ${AGENTBIN} ${AGENTDIR}/bin/${AGENTBIN} +install -D -T -m 755 ${TOKENSCRIPT} ${AGENTDIR}/bin/${TOKENSCRIPT} +install -T -m 755 ${TOKENSCRIPT}.service ${SYSTEMDFILES}/${TOKENSCRIPT}.service +install -T -m 755 ${AGENTSCRIPT}.service ${SYSTEMDFILES}/${AGENTSCRIPT}.service + +systemctl enable ${TOKENSCRIPT} +systemctl start ${TOKENSCRIPT} + +systemctl enable ${AGENTSCRIPT} +systemctl start ${AGENTSCRIPT} + +exit 0 diff --git a/aws_secretsmanager_agent/configuration/uninstall b/aws_secretsmanager_agent/configuration/uninstall new file mode 100755 index 0000000..ea22583 --- /dev/null +++ b/aws_secretsmanager_agent/configuration/uninstall @@ -0,0 +1,23 @@ +#!/bin/bash -x + +if [ `id -u` -ne 0 ]; then + echo "This script must be run as root" >&2 + exit 1 +fi + + +AGENTDIR=/opt/aws/secretsmanageragent +TOKENGROUP=awssmatokenreader +AGENTUSER=awssmauser +TOKENSCRIPT=awssmaseedtoken +AGENTSCRIPT=awssmastartup + +systemctl stop awssmastartup +systemctl disable awssmastartup +systemctl stop awssmaseedtoken +systemctl disable awssmaseedtoken +rm -f /etc/systemd/system/${TOKENSCRIPT}.service /etc/systemd/system/${AGENTSCRIPT}.service + +rm -rf ${AGENTDIR} +userdel -r ${AGENTUSER} +groupdel ${TOKENGROUP} diff --git a/aws_secretsmanager_agent/examples/example-lambda-extension/secrets-manager-agent-extension.sh b/aws_secretsmanager_agent/examples/example-lambda-extension/secrets-manager-agent-extension.sh new file mode 100644 index 0000000..66ab600 --- /dev/null +++ b/aws_secretsmanager_agent/examples/example-lambda-extension/secrets-manager-agent-extension.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +set -euo pipefail + +OWN_FILENAME="$(basename $0)" +LAMBDA_EXTENSION_NAME="$OWN_FILENAME" # (external) extension name has to match the filename +TMPFILE=/tmp/$OWN_FILENAME + +# Graceful Shutdown +_term() { + echo "[${LAMBDA_EXTENSION_NAME}] Received EXIT" + # forward EXIT to child procs and exit + kill -TERM "$PID" 2>/dev/null + echo "[${LAMBDA_EXTENSION_NAME}] Exiting" + exit 0 +} + +forward_exit_and_wait() { + trap _term EXIT + wait "$PID" + trap - EXIT +} + +start_agent() { + if [ -z "${AGENT_PID:-}" ]; then + echo "[${LAMBDA_EXTENSION_NAME}] Starting Secrets Manager Agent." + # Switching working directory to ensure that the logs are written to a folder that has write permissions. + (cd /tmp && /opt/bin/secrets-manager-agent &) + AGENT_PID=$! + + echo "[${LAMBDA_EXTENSION_NAME}] Checking if the Agent is serving requests." + RETRIES=0 + MAX_RETRIES=200 + while true; do + RESPONSE=$(curl -s http://localhost:2773/ping || echo "Agent has not started yet.") + if [ "$RESPONSE" = "healthy" ]; then + echo "[${LAMBDA_EXTENSION_NAME}] Agent has started." + break + else + if [ $RETRIES -ge $MAX_RETRIES ]; then + echo "[${LAMBDA_EXTENSION_NAME}] Agent failed to start after $MAX_RETRIES retries." + exit 1 + fi + echo "[${LAMBDA_EXTENSION_NAME}] Agent has not started yet, retrying in 100 milliseconds..." + sleep 0.1 # Sleep for 100 milliseconds + fi + done + else + echo "[${LAMBDA_EXTENSION_NAME}] Agent already started, ignoring INVOKE event." + fi +} + + +stop_agent() { + if [ -n "$AGENT_PID" ]; then + echo "[${LAMBDA_EXTENSION_NAME}] Stopping the Secrets Manager Agent." + kill "$AGENT_PID" 2>/dev/null + unset AGENT_PID + else + echo "[${LAMBDA_EXTENSION_NAME}] Agent not running. Nothing to stop." + fi +} + + +# Initialization +# To run any extension processes that need to start before the runtime initializes, run them before the /register +echo "[${LAMBDA_EXTENSION_NAME}] Initialization" + +# Registration +# The extension registration also signals to Lambda to start initializing the runtime. +HEADERS="$(mktemp)" +echo "[${LAMBDA_EXTENSION_NAME}] Registering at http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/register" +curl -sS -LD "$HEADERS" -XPOST "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/register" --header "Lambda-Extension-Name: ${LAMBDA_EXTENSION_NAME}" -d "{ \"events\": [\"INVOKE\", \"SHUTDOWN\"]}" > $TMPFILE + +RESPONSE=$(<$TMPFILE) +HEADINFO=$(<$HEADERS) +# Extract Extension ID from response headers +EXTENSION_ID=$(grep -Fi Lambda-Extension-Identifier "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) +echo "[${LAMBDA_EXTENSION_NAME}] Registration response: ${RESPONSE} with EXTENSION_ID ${EXTENSION_ID}" + +# Event processing +# Continuous loop to wait for events from Extensions API +while true +do + echo "[${LAMBDA_EXTENSION_NAME}] Waiting for event. Get /next event from http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/event/next" + + # Get an event. The HTTP request will block until one is received + curl -sS -L -XGET "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/event/next" --header "Lambda-Extension-Identifier: ${EXTENSION_ID}" > $TMPFILE & + PID=$! + forward_exit_and_wait + + EVENT_DATA=$(<$TMPFILE) + if [[ $EVENT_DATA == *"INVOKE"* ]]; then + echo "[extension: ${LAMBDA_EXTENSION_NAME}] Received INVOKE event." + + # Starting the Secrets Manager Agent + # The agent is initialized AFTER the first Invoke phase, instead of right AFTER Init phase. + # This prevents users from calling for secrets before the Init phase has finished. + # Initializing the agent during Init would allow users to call and cache secrets before the snapshot phase (occurs last in Init), which in turn could store those values + # in a snapshot of the sandbox for up to 14 days. https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html + # Implement retry logic in your application code, to accommodate delays in agent initialization. + start_agent + fi + + if [[ $EVENT_DATA == *"SHUTDOWN"* ]]; then + echo "[extension: ${LAMBDA_EXTENSION_NAME}] Received SHUTDOWN event. Exiting." + stop_agent + exit 0 # Exit if we receive a SHUTDOWN event + fi + +done diff --git a/aws_secretsmanager_agent/src/cache_manager.rs b/aws_secretsmanager_agent/src/cache_manager.rs new file mode 100644 index 0000000..85849fc --- /dev/null +++ b/aws_secretsmanager_agent/src/cache_manager.rs @@ -0,0 +1,329 @@ +use crate::error::HttpError; +use crate::utils::err_response; +use aws_sdk_secretsmanager::error::ProvideErrorMetadata; +use aws_sdk_secretsmanager::operation::describe_secret::DescribeSecretError; +use aws_sdk_secretsmanager::operation::get_secret_value::GetSecretValueError; +use aws_secretsmanager_caching::SecretsManagerCachingClient; +use aws_smithy_runtime_api::client::orchestrator::HttpResponse; +use aws_smithy_runtime_api::client::result::SdkError; +use log::error; + +use crate::config::Config; + +/// Wrapper around the caching library +/// +/// Used to cache and retrieve secrets. +#[derive(Debug)] +pub struct CacheManager(SecretsManagerCachingClient); + +// Use either the real Secrets Manager client or the stub for testing +#[doc(hidden)] +#[cfg(not(test))] +use crate::utils::validate_and_create_asm_client as asm_client; +#[cfg(test)] +use tests::init_client as asm_client; + +/// Wrapper around the caching library +/// +/// Used to cache and retrieve secrets. +impl CacheManager { + /// Create a new CacheManager. For simplicity I'm propagating the errors back up for now. + pub async fn new(cfg: &Config) -> Result> { + Ok(Self(SecretsManagerCachingClient::new( + asm_client(cfg).await?, + cfg.cache_size(), + cfg.ttl(), + )?)) + } + + /// Fetch a secret from the cache. + /// + /// # Arguments + /// + /// * `name` - The name of the secret to fetch. + /// * `version` - The version of the secret to fetch. + /// * `label` - The label of the secret to fetch. + /// + /// # Returns + /// + /// * `Ok(String)` - The value of the secret. + /// * `Err((u16, String))` - The error code and message. + /// + /// # Errors + /// + /// * `SerializationError` - The error returned from the serde_json::to_string method. + /// + /// # Example + /// + /// ``` + /// let cache_manager = CacheManager::new().await.unwrap(); + /// let value = cache_manager.fetch("my-secret", None, None).unwrap(); + /// ``` + pub async fn fetch( + &self, + secret_id: &str, + version: Option<&str>, + label: Option<&str>, + ) -> Result { + // Read the secret from the cache or fetch it over the network. + let found = match self.0.get_secret_value(secret_id, version, label).await { + Ok(value) => value, + Err(e) if e.is::>() => { + let (code, msg, status) = svc_err::(e)?; + return Err(HttpError(status, err_response(&code, &msg))); + } + Err(e) if e.is::>() => { + let (code, msg, status) = svc_err::(e)?; + return Err(HttpError(status, err_response(&code, &msg))); + } + Err(e) => { + error!("Internal error for {secret_id} - {:?}", e); + return Err(int_err()); + } + }; + + // Serialize and return the value + match serde_json::to_string(&found) { + Ok(value) => Ok(value), + _ => { + error!("Serialization error for {secret_id}"); + Err(int_err())? + } + } + } +} + +/// Private helper to format in internal service error response. +#[doc(hidden)] +fn int_err() -> HttpError { + HttpError(500, err_response("InternalFailure", "")) +} + +/// Private helper to extract the error code, message, and status code from an SDK exception. +/// +/// Downcasts the exception into the specific SDK exception type and retrieves +/// the excpetion code (e.g. ResourceNotFoundException), error message, and http +/// status code or returns an error if the fields are not present. Timeout and +/// network errors are also translated to appropriate error codes. +/// +/// # Returns +/// +/// * `Ok((code, msg, status))` - A tuple of error code, error message, and http status code. +/// * `Err((500, InternalFailureString))` - An internal service error. +#[doc(hidden)] +fn svc_err(err: Box) -> Result<(String, String, u16), HttpError> +where + S: ProvideErrorMetadata + std::error::Error + 'static, +{ + let sdk_err = err + .downcast_ref::>() + .ok_or(int_err())?; + + // Get the error metadata and translate timeouts to 504 and network errors to 502 + let err_meta = match sdk_err { + SdkError::ServiceError(serr) => serr.err().meta(), + SdkError::DispatchFailure(derr) if derr.is_timeout() => { + return Ok(("TimeoutError".into(), "Timeout".into(), 504)); + } + SdkError::TimeoutError(_) => { + return Ok(("TimeoutError".into(), "Timeout".into(), 504)); + } + SdkError::DispatchFailure(derr) if derr.is_io() => { + return Ok(("ConnectionError".into(), "Read Error".into(), 502)); + } + SdkError::ResponseError(_) => { + return Ok(("ConnectionError".into(), "Response Error".into(), 502)); + } + _ => return Err(int_err()), + }; + + let code = err_meta.code().ok_or(int_err())?; + let msg = err_meta.message().ok_or(int_err())?; + let status = sdk_err.raw_response().ok_or(int_err())?.status().as_u16(); + + Ok((code.into(), msg.into(), status)) +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::constants::APPNAME; + use crate::utils::AgentModifierInterceptor; + use aws_config::BehaviorVersion; + use aws_sdk_secretsmanager as secretsmanager; + use aws_smithy_runtime::client::http::test_util::{infallible_client_fn, NeverClient}; + use aws_smithy_types::body::SdkBody; + use core::time::Duration; + use http::{Request, Response}; + use serde_json::Value; + use std::thread::sleep; + + use std::cell::RefCell; + use std::thread_local; + + pub const FAKE_ARN: &str = + "arn:aws:secretsmanager:us-west-2:123456789012:secret:{{name}}-NhBWsc"; + pub const DEFAULT_VERSION: &str = "5767290c-d089-49ed-b97c-17086f8c9d79"; + pub const DEFAULT_LABEL: &str = "AWSCURRENT"; + + // Template GetSecretValue responses for testing + const GSV_BODY: &str = r###"{ + "ARN": "{{arn}}", + "Name": "{{name}}", + "VersionId": "{{version}}", + "SecretString": "hunter2", + "VersionStages": [ + "{{label}}" + ], + "CreatedDate": 1569534789.046 + }"###; + + // Template DescribeSecret responses for testing + const DESC_BODY: &str = r###"{ + "ARN": "{{arn}}", + "Name": "{{name}}", + "Description": "My test secret", + "KmsKeyId": "arn:aws:kms:us-west-2:123456789012:key/exampled-90ab-cdef-fedc-bbd6-7e6f303ac933", + "LastChangedDate": 1523477145.729, + "LastAccessedDate": 1524572133.25, + "VersionIdsToStages": { + "{{version}}": [ + "{{label}}" + ] + }, + "CreatedDate": 1569534789.046 + }"###; + + // Template for access denied testing + const KMS_ACCESS_DENIED_BODY: &str = r###"{ + "__type":"AccessDeniedException", + "Message":"Access to KMS is not allowed" + }"###; + + // Template for testing other errors (bad creds in this case) + const OTHER_EXCEPTION_BODY: &str = r###"{ + "__type":"InvalidSignatureException", + "message":"The request signature we calculated does not match ..." + }"###; + + // Template for testing resource not found with DescribeSecret + const NOT_FOUND_EXCEPTION_BODY: &str = r###"{ + "__type":"ResourceNotFoundException", + "message":"Secrets Manager can't find the specified secret." + }"###; + + // Used to inject a test client to stub off Secrets Manager network calls. + thread_local! { + static CLIENT: RefCell = RefCell::new(def_fake_client()); + } + + // Test interface to override the default client used. + pub fn set_client(client: secretsmanager::Client) { + CLIENT.set(client); + } + + // Used to replace the real client with the stub client. + pub async fn init_client( + _cfg: &Config, + ) -> Result> { + Ok(CLIENT.with_borrow(|v| v.clone())) + } + + // Private helper to look at the request and provide the correct reponse. + fn format_rsp(req: Request) -> (u16, String) { + let (parts, body) = req.into_parts(); + assert!(parts.headers["user-agent"] + .to_str() + .unwrap() + .contains(APPNAME)); // validate user-agent + + let req_map: serde_json::Map = + serde_json::from_slice(body.bytes().unwrap()).unwrap(); + let version = req_map + .get("VersionId") + .map_or(DEFAULT_VERSION, |x| x.as_str().unwrap()); + let label = req_map + .get("VersionStage") + .map_or(DEFAULT_LABEL, |x| x.as_str().unwrap()); + let name = req_map.get("SecretId").unwrap().as_str().unwrap(); // Does not handle full ARN case. + + let (code, template) = match parts.headers["x-amz-target"].to_str().unwrap() { + "secretsmanager.GetSecretValue" if name.starts_with("KMSACCESSDENIED") => { + (400, KMS_ACCESS_DENIED_BODY) + } + "secretsmanager.GetSecretValue" if name.starts_with("OTHERERROR") => { + (400, OTHER_EXCEPTION_BODY) + } + "secretsmanager.DescribeSecret" if name.starts_with("NOTFOUND") => { + (400, NOT_FOUND_EXCEPTION_BODY) + } + "secretsmanager.GetSecretValue" => (200, GSV_BODY), + "secretsmanager.DescribeSecret" => (200, DESC_BODY), + _ => panic!("Unknown operation"), + }; + + // Implement a sleep for testing. We can not do an async sleep here so + // timeout tests should use the timeout_client instead. + if let Some(sleep_val) = name.strip_prefix("SleepyTest_") { + if let Ok(sleep_num) = sleep_val.parse::() { + sleep(Duration::from_secs(sleep_num)); + } + } + + // Fill in the template and return the response. + let rsp = template + .replace("{{arn}}", FAKE_ARN) + .replace("{{name}}", name) + .replace("{{version}}", version) + .replace("{{label}}", label); + (code, rsp) + } + + // Test client that stubs off network call and provides a canned response. + fn def_fake_client() -> secretsmanager::Client { + let fake_creds = secretsmanager::config::Credentials::new( + "AKIDTESTKEY", + "astestsecretkey", + Some("atestsessiontoken".to_string()), + None, + "", + ); + let http_client = infallible_client_fn(|_req| { + let (code, rsp) = format_rsp(_req); + Response::builder() + .status(code) + .body(SdkBody::from(rsp)) + .unwrap() + }); + + secretsmanager::Client::from_conf( + secretsmanager::Config::builder() + .behavior_version(BehaviorVersion::latest()) + .credentials_provider(fake_creds) + .interceptor(AgentModifierInterceptor) + .region(secretsmanager::config::Region::new("us-west-2")) + .http_client(http_client) + .build(), + ) + } + + // Test client that makes all Secrets Manager calls time out. + pub fn timeout_client() -> secretsmanager::Client { + let fake_creds = secretsmanager::config::Credentials::new( + "AKIDTESTKEY", + "astestsecretkey", + Some("atestsessiontoken".to_string()), + None, + "", + ); + + secretsmanager::Client::from_conf( + secretsmanager::Config::builder() + .behavior_version(BehaviorVersion::latest()) + .credentials_provider(fake_creds) + .region(secretsmanager::config::Region::new("us-west-2")) + .http_client(NeverClient::new()) + .build(), + ) + } +} diff --git a/aws_secretsmanager_agent/src/config.rs b/aws_secretsmanager_agent/src/config.rs new file mode 100644 index 0000000..9f19f16 --- /dev/null +++ b/aws_secretsmanager_agent/src/config.rs @@ -0,0 +1,600 @@ +use crate::constants::EMPTY_ENV_LIST_MSG; +use crate::constants::{BAD_MAX_CONN_MSG, BAD_PREFIX_MSG, EMPTY_SSRF_LIST_MSG}; +use crate::constants::{DEFAULT_MAX_CONNECTIONS, GENERIC_CONFIG_ERR_MSG}; +use crate::constants::{INVALID_CACHE_SIZE_ERR_MSG, INVALID_HTTP_PORT_ERR_MSG}; +use crate::constants::{INVALID_LOG_LEVEL_ERR_MSG, INVALID_TTL_SECONDS_ERR_MSG}; +use config::Config as ConfigLib; +use config::File; +use serde_derive::Deserialize; +use std::num::NonZeroUsize; +use std::ops::Range; +use std::str::FromStr; +use std::time::Duration; + +const DEFAULT_LOG_LEVEL: &str = "info"; +const DEFAULT_HTTP_PORT: &str = "2773"; +const DEFAULT_TTL_SECONDS: &str = "300"; +const DEFAULT_CACHE_SIZE: &str = "1000"; +const DEFAULT_SSRF_HEADERS: [&str; 2] = ["X-Aws-Parameters-Secrets-Token", "X-Vault-Token"]; +const DEFAULT_SSRF_ENV_VARIABLES: [&str; 2] = ["AWS_TOKEN", "AWS_SESSION_TOKEN"]; +const DEFAULT_PATH_PREFIX: &str = "/v1/"; + +const DEFAULT_REGION: Option = None; + +/// Private struct used to deserialize configurations from the file. +#[doc(hidden)] +#[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] // We want to error out when file has misspelled or unknown configurations. +struct ConfigFile { + log_level: String, + http_port: String, + ttl_seconds: String, + cache_size: String, + ssrf_headers: Vec, + ssrf_env_variables: Vec, + path_prefix: String, + max_conn: String, + region: Option, +} + +/// The log levels supported by the daemon. +#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Copy)] +pub enum LogLevel { + Debug, + Info, + Warn, + Error, + None, +} + +/// Returns the log level if the provided `log_level` string is valid. +/// Returns Err if it's invalid. +impl FromStr for LogLevel { + type Err = String; + fn from_str(log_level: &str) -> Result { + match log_level.to_lowercase().as_str() { + "debug" => Ok(LogLevel::Debug), + "info" => Ok(LogLevel::Info), + "warn" => Ok(LogLevel::Warn), + "error" => Ok(LogLevel::Error), + "none" => Ok(LogLevel::None), + _ => Err(String::from(INVALID_LOG_LEVEL_ERR_MSG)), + } + } +} + +/// The contains the configurations that are used by the daemon. +#[derive(Debug, Deserialize, Clone)] +pub struct Config { + /// The level of logging the agent provides ie. debug, info, warn, error or none. + log_level: LogLevel, + + /// The port for the local HTTP server. + http_port: u16, + + /// The `time to live` of a secret + ttl: Duration, + + /// Maximum number secrets that can be stored in the cache. + cache_size: NonZeroUsize, + + /// A list of request headers which will be checked in order for the SSRF + /// token. Contains at least one request header. + ssrf_headers: Vec, + + /// The list of the environment variable names to search through for the SSRF token. + ssrf_env_variables: Vec, + + /// The prefix for path based requests. + path_prefix: String, + + /// The maximum number of simultaneous connections. + max_conn: usize, + + /// The AWS Region that will be used to send the Secrets Manager request to. + region: Option, +} + +/// The default configuration options. +impl Default for Config { + fn default() -> Self { + Config::new(None).expect(GENERIC_CONFIG_ERR_MSG) + } +} + +/// The contains the configurations that are used by the daemon. +impl Config { + /// Initialize the configuation using the optional configuration file. + /// + /// If and override file is not provided, default configurations will be + /// used. + /// + /// # Arguments + /// + /// * `file_pth` - The configuration file (in toml format) used to override the default, or None to use the defaults. + /// + /// # Returns + /// + /// * `Ok(Config)` - The config struct. + /// * `Err((Error)` - The error encountered when trying to read or parse the config overrides. + pub fn new(file_path: Option<&str>) -> Result> { + // Setting default configurations + let mut config = ConfigLib::builder() + .set_default("log_level", DEFAULT_LOG_LEVEL)? + .set_default("http_port", DEFAULT_HTTP_PORT)? + .set_default("ttl_seconds", DEFAULT_TTL_SECONDS)? + .set_default("cache_size", DEFAULT_CACHE_SIZE)? + .set_default::<&str, Vec>( + "ssrf_headers", + DEFAULT_SSRF_HEADERS.map(String::from).to_vec(), + )? + .set_default::<&str, Vec>( + "ssrf_env_variables", + DEFAULT_SSRF_ENV_VARIABLES.map(String::from).to_vec(), + )? + .set_default("path_prefix", DEFAULT_PATH_PREFIX)? + .set_default("max_conn", DEFAULT_MAX_CONNECTIONS)? + .set_default("region", DEFAULT_REGION)?; + + // Merge the config overrides onto the default configurations, if provided. + config = match file_path { + Some(file_path_str) => config.add_source(File::with_name(file_path_str)), + None => config, + }; + + Config::build(config.build()?.try_deserialize()?) + } + + /// The level of logging the agent provides ie. debug, info, warn, error or none + /// + /// # Returns + /// + /// * `LogLevel` - The log level to use. Defaults to Info. + pub fn log_level(&self) -> LogLevel { + self.log_level + } + + /// The port for the local HTTP server to listen for incomming requests. + /// + /// # Returns + /// + /// * `port` - The TCP port number. Defaults to 2773. + pub fn http_port(&self) -> u16 { + self.http_port + } + + /// The `time to live` of a secret in the cache in seconds. + /// + /// # Returns + /// + /// * `ttl` - The number of seconds to retain a secret in the cache. Defaults to 300. + pub fn ttl(&self) -> Duration { + self.ttl + } + + /// Maximum number secrets that can be stored in the cache + /// + /// # Returns + /// + /// * `cache_size` - The maximum number of secrets to cache. Defaults to 1000. + pub fn cache_size(&self) -> NonZeroUsize { + self.cache_size + } + + /// A list of request headers which will be checked for the SSRF token (can not be empty). + /// + /// # Returns + /// + /// * `ssrf_headers` - List of headers to check for SSRF token. Defaults to ["X-Aws-Parameters-Secrets-Token", "X-Vault-Token"]. + pub fn ssrf_headers(&self) -> Vec { + self.ssrf_headers.clone() + } + + /// The name of the environment variable containing the SSRF token. + /// + /// # Returns + /// + /// * `ssrf_env_variables` - The name of the env variable containing the SSRF token value. Defaults to ["AWS_TOKEN", "AWS_SESSION_TOKEN"]. + pub fn ssrf_env_variables(&self) -> Vec { + self.ssrf_env_variables.clone() + } + + /// The prefix for path based requests (must begin with /). + /// + /// # Returns + /// + /// * `path_prefix` - The path name prefix. Defaults to /v1/. + pub fn path_prefix(&self) -> String { + self.path_prefix.clone() + } + + /// The maximum number of simultaneous connections (1000 max). + /// + /// # Returns + /// + /// * `max_conn` - The maximum allowed simultaneious connections. Defaults to 800. + pub fn max_conn(&self) -> usize { + self.max_conn + } + + /// The AWS Region that will be used to send the Secrets Manager request to. + /// The default region is automatically determined through SDK defaults. + /// For a list of all of the Regions that you can specify, see https://docs.aws.amazon.com/general/latest/gr/asm.html + /// + /// # Returns + /// + /// * `region` - The AWS Region that will be used to send the Secrets Manager request to. + pub fn region(&self) -> Option<&String> { + self.region.as_ref() + } + + /// Private helper that fills in the Config instance from the specified + /// config overrides (or defaults). + /// + /// # Arguments + /// + /// * `config_file` - The parsed config overrides and defaults. + /// + /// # Returns + /// + /// * `Ok(Config)` - If no errors were found in the overrides. + /// * `Err(Error)` - An error message with the configuration error. + #[doc(hidden)] + fn build(config_file: ConfigFile) -> Result> { + let config = Config { + // Configurations that are allowed to be overridden. + log_level: LogLevel::from_str(config_file.log_level.as_str())?, + http_port: parse_num::( + &config_file.http_port, + INVALID_HTTP_PORT_ERR_MSG, + None, + Some(1..1024), + )?, + ttl: Duration::from_secs(parse_num::( + &config_file.ttl_seconds, + INVALID_TTL_SECONDS_ERR_MSG, + Some(0..3601), + None, + )?), + cache_size: match NonZeroUsize::new(parse_num::( + &config_file.cache_size, + INVALID_CACHE_SIZE_ERR_MSG, + Some(0..1001), + None, + )?) { + Some(x) => x, + None => Err(INVALID_CACHE_SIZE_ERR_MSG)?, + }, + ssrf_headers: config_file.ssrf_headers, + ssrf_env_variables: config_file.ssrf_env_variables, + path_prefix: config_file.path_prefix, + max_conn: parse_num::( + &config_file.max_conn, + BAD_MAX_CONN_MSG, + Some(1..1001), + None, + )?, + region: config_file.region, + }; + + // Additional validations. + if config.ssrf_headers.is_empty() { + Err(EMPTY_SSRF_LIST_MSG)?; + } + if config.ssrf_env_variables.is_empty() { + Err(EMPTY_ENV_LIST_MSG)?; + } + if !config.path_prefix.starts_with('/') { + Err(BAD_PREFIX_MSG)?; + } + + Ok(config) + } +} + +/// Private helper to convert a string to number and perform range checks, returning a custom error on failure. +/// +/// # Arguments +/// +/// * `str_val` - The sring to convert. +/// * `msg` - The custom error message. +/// * `pos_range` - An optional positive range constraint. The number must be within this range. +/// * `neg_range` - An optional negitive range constraint. The number must not be within this range. +/// +/// # Returns +/// +/// * `Ok(num)` - When the string can be parsed and the number satisfies the range checks. +/// * `Err(Error)` - The custom error message on failure. +/// +/// # Example +/// +/// ``` +/// use std::ops::Range; +/// assert_eq!(parse_num::(&String::from("42"), "What is the qustion?", Some(1..100), None).unwrap(), 42); +/// ``` +#[doc(hidden)] +fn parse_num( + str_val: &str, + msg: &str, + pos_range: Option>, + neg_range: Option>, +) -> Result> +where + T: PartialOrd + Sized + std::str::FromStr, +{ + let val = match str_val.parse::() { + Ok(x) => x, + _ => Err(msg)?, + }; + if let Some(rng) = pos_range { + if !rng.contains(&val) { + Err(msg)?; + } + } + if let Some(rng) = neg_range { + if rng.contains(&val) { + Err(msg)?; + } + } + + Ok(val) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + /// Test helper function that returns the a ConfigFile with default values. + fn get_default_config_file() -> ConfigFile { + ConfigFile { + log_level: String::from(DEFAULT_LOG_LEVEL), + http_port: String::from(DEFAULT_HTTP_PORT), + ttl_seconds: String::from(DEFAULT_TTL_SECONDS), + cache_size: String::from(DEFAULT_CACHE_SIZE), + ssrf_headers: DEFAULT_SSRF_HEADERS.map(String::from).to_vec(), + ssrf_env_variables: DEFAULT_SSRF_ENV_VARIABLES.map(String::from).to_vec(), + path_prefix: String::from(DEFAULT_PATH_PREFIX), + max_conn: String::from(DEFAULT_MAX_CONNECTIONS), + region: None, + } + } + + /// Tests that the default configurations are correct. + #[test] + fn test_default_config() { + let config = Config::default(); + assert_eq!(config.clone().log_level(), LogLevel::Info); + assert_eq!(config.clone().http_port(), 2773); + assert_eq!(config.clone().ttl(), Duration::from_secs(300)); + assert_eq!( + config.clone().cache_size(), + NonZeroUsize::new(1000).unwrap() + ); + assert_eq!( + config.clone().ssrf_headers(), + DEFAULT_SSRF_HEADERS.map(String::from).to_vec() + ); + assert_eq!( + config.clone().ssrf_env_variables(), + DEFAULT_SSRF_ENV_VARIABLES.map(String::from).to_vec() + ); + assert_eq!(config.clone().path_prefix(), DEFAULT_PATH_PREFIX); + assert_eq!(config.clone().max_conn(), 800); + assert_eq!(config.clone().region(), None); + } + + /// Tests the config overrides are applied correctly from the provided config file. + #[test] + fn test_config_overrides() { + let config = Config::new(Some("tests/resources/configs/config_file_valid.toml")).unwrap(); + assert_eq!(config.clone().log_level(), LogLevel::Debug); + assert_eq!(config.clone().http_port(), 65535); + assert_eq!(config.clone().ttl(), Duration::from_secs(300)); + assert_eq!( + config.clone().cache_size(), + NonZeroUsize::new(1000).unwrap() + ); + assert_eq!( + config.clone().ssrf_headers(), + vec!("X-Aws-Parameters-Secrets-Token".to_string()) + ); + assert_eq!( + config.clone().ssrf_env_variables(), + vec!("MY_TOKEN".to_string()) + ); + assert_eq!(config.clone().path_prefix(), "/other"); + assert_eq!(config.clone().max_conn(), 10); + assert_eq!(config.clone().region(), Some(&"us-west-2".to_string())); + } + + /// Tests that an Err is returned when an invalid value is provided in one of the configurations. + #[test] + fn test_config_overrides_invalid_value() { + match Config::new(Some( + "tests/resources/configs/config_file_with_invalid_config.toml", + )) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), INVALID_LOG_LEVEL_ERR_MSG), + }; + } + + /// Tests that an valid log level values don't return an Err. + #[test] + fn test_validate_config_valid_log_level_values() { + let mut input_output_map = HashMap::new(); + input_output_map.insert("info".to_string(), LogLevel::Info); + input_output_map.insert("Info".to_string(), LogLevel::Info); + input_output_map.insert("INFO".to_string(), LogLevel::Info); + input_output_map.insert("debug".to_string(), LogLevel::Debug); + input_output_map.insert("Debug".to_string(), LogLevel::Debug); + input_output_map.insert("DEBUG".to_string(), LogLevel::Debug); + input_output_map.insert("warn".to_string(), LogLevel::Warn); + input_output_map.insert("Warn".to_string(), LogLevel::Warn); + input_output_map.insert("WARN".to_string(), LogLevel::Warn); + input_output_map.insert("error".to_string(), LogLevel::Error); + input_output_map.insert("Error".to_string(), LogLevel::Error); + input_output_map.insert("ERROR".to_string(), LogLevel::Error); + input_output_map.insert("none".to_string(), LogLevel::None); + input_output_map.insert("None".to_string(), LogLevel::None); + input_output_map.insert("NONE".to_string(), LogLevel::None); + + for (input, output) in input_output_map.iter() { + let invalid_config = ConfigFile { + log_level: input.clone(), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(actual) => assert_eq!(actual.log_level(), output.clone()), + Err(_) => panic!(), + }; + } + } + + /// Tests that an invalid log level returns an Err + #[test] + fn test_validate_config_invalid_log_level() { + let invalid_config = ConfigFile { + log_level: String::from("information"), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), INVALID_LOG_LEVEL_ERR_MSG), + }; + } + + /// Tests that an invalid http port value returns an Err + #[test] + fn test_validate_config_http_port_invalid_values() { + for value in ["1023", "-1", "65536", "not a number"] { + let invalid_config = ConfigFile { + http_port: String::from(value), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), INVALID_HTTP_PORT_ERR_MSG), + }; + } + } + + /// Tests that an invalid max conn value returns an Err + #[test] + fn test_validate_config_max_conn_invalid_values() { + for value in ["1001", "-1", "0", "not a number"] { + let invalid_config = ConfigFile { + max_conn: String::from(value), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), BAD_MAX_CONN_MSG), + }; + } + } + + /// Tests that an invalid ttl_seconds value returns an Err + #[test] + fn test_validate_ttl_seconds_invalid_values() { + for value in ["-1", "3601", "not a number"] { + let invalid_config = ConfigFile { + ttl_seconds: String::from(value), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), INVALID_TTL_SECONDS_ERR_MSG), + }; + } + } + + /// Tests that an invalid cache_size value returns an Err + #[test] + fn test_validate_cache_size_invalid_values() { + for value in ["-1", "0", "1001", "not a number"] { + let invalid_config = ConfigFile { + cache_size: String::from(value), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), INVALID_CACHE_SIZE_ERR_MSG), + }; + } + } + + /// Tests that an invalid ssrf header list returns an Err + #[test] + fn test_validate_ssrf_headers() { + let invalid_config = ConfigFile { + ssrf_headers: Vec::new(), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), EMPTY_SSRF_LIST_MSG), + }; + } + + /// Tests that an invalid env variable list returns an Err + #[test] + fn test_validate_ssrf_env_variables() { + let invalid_config = ConfigFile { + ssrf_env_variables: Vec::new(), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), EMPTY_ENV_LIST_MSG), + }; + } + + /// Tests that an invalid path prefix returns an Err + #[test] + fn test_validate_path_prefix() { + let invalid_config = ConfigFile { + path_prefix: String::from("v1"), + ..get_default_config_file() + }; + match Config::build(invalid_config) { + Ok(_) => panic!(), + Err(e) => assert_eq!(e.to_string(), BAD_PREFIX_MSG), + }; + } + + /// Tests that an empty config file does not return an error and the default configuration are used. + #[test] + fn test_config_empty_config_file() { + let config = Config::new(Some("tests/resources/configs/config_file_empty.toml")).unwrap(); + assert_eq!(config.clone().log_level(), LogLevel::Info); + assert_eq!(config.clone().http_port(), 2773); + assert_eq!(config.clone().ttl(), Duration::from_secs(300)); + assert_eq!( + config.clone().cache_size(), + NonZeroUsize::new(1000).unwrap() + ); + } + + /// Tests that a wrong file path returns an Err with appropriate message + #[test] + fn test_config_wrong_config_file_path() { + match Config::new(Some("file_does_not_exist")) { + Ok(_) => panic!(), + Err(e) => assert_eq!( + e.to_string(), + "configuration file \"file_does_not_exist\" not found" + ), + }; + } + + /// Tests that a config file with invalid content returns an Err with appropriate message. + #[test] + #[should_panic(expected = "TOML parse error")] + fn test_config_invalid_file_contents() { + Config::new(Some( + "tests/resources/configs/config_file_with_invalid_contents.toml", + )) + .unwrap(); + } +} diff --git a/aws_secretsmanager_agent/src/constants.rs b/aws_secretsmanager_agent/src/constants.rs new file mode 100644 index 0000000..9d4edde --- /dev/null +++ b/aws_secretsmanager_agent/src/constants.rs @@ -0,0 +1,27 @@ +/// User visible error messages. +pub const INVALID_LOG_LEVEL_ERR_MSG: &str = "The log level specified in the configuration file isn't valid. The log level must be DEBUG, INFO, WARN, ERROR, or NONE."; +pub const INVALID_HTTP_PORT_ERR_MSG: &str = "The HTTP port specified in the configuration file isn't valid. The HTTP port must be in the range 1024 to 65535."; +pub const INVALID_TTL_SECONDS_ERR_MSG: &str = "The TTL in seconds specified in the configuration file isn't valid. The TTL in seconds must be in the range 1 to 3600."; +pub const INVALID_CACHE_SIZE_ERR_MSG: &str = "The cache size specified in the configuration file isn't valid. The cache size must be in the range 1 to 1000."; +pub const GENERIC_CONFIG_ERR_MSG: &str = + "There was an unexpected error in loading the configuration file."; +pub const BAD_MAX_CONN_MSG: &str = "The maximum number of connections specified in the configuration file isn't valid. The maximum number of connections must be in the range 1 to 1000."; +pub const EMPTY_SSRF_LIST_MSG: &str = + "The list of SSRF headers in the configuration file can't be empty."; +pub const EMPTY_ENV_LIST_MSG: &str = + "The list of SSRF environment variables in the configuration file can't be empty."; +pub const BAD_PREFIX_MSG: &str = + "The path prefix specified in the configuration file must begin with /."; + +/// Other constants that are used across the code base. + +// The application name. +pub const APPNAME: &str = "aws-secrets-manager-agent"; +// The build version of the agent +pub const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); +// The maximum for incoming connections need to be relatively high, since during periods of high latency, we can easily have many outstanding connections on a very busy box. +pub const DEFAULT_MAX_CONNECTIONS: &str = "800"; +// The max request time +pub const MAX_REQ_TIME_SEC: u64 = 61; +// The max buffer size +pub const MAX_BUF_BYTES: usize = (65 + 256) * 1024; // 321 KB diff --git a/aws_secretsmanager_agent/src/error.rs b/aws_secretsmanager_agent/src/error.rs new file mode 100644 index 0000000..e73ff7d --- /dev/null +++ b/aws_secretsmanager_agent/src/error.rs @@ -0,0 +1,20 @@ +#[derive(Debug)] +pub(crate) struct HttpError(pub u16, pub String); + +impl From for HttpError { + fn from(e: url::ParseError) -> Self { + HttpError(400, e.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_error() { + let error = HttpError::from(url::ParseError::Overflow); + assert_eq!(400, error.0); + assert_eq!("URLs more than 4 GB are not supported", error.1); + } +} diff --git a/aws_secretsmanager_agent/src/logging.rs b/aws_secretsmanager_agent/src/logging.rs new file mode 100644 index 0000000..efb4405 --- /dev/null +++ b/aws_secretsmanager_agent/src/logging.rs @@ -0,0 +1,117 @@ +use crate::config::LogLevel; +use log::{info, LevelFilter, SetLoggerError}; +use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller; +use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger; +use log4rs::append::rolling_file::policy::compound::CompoundPolicy; +use log4rs::append::rolling_file::RollingFileAppender; +use log4rs::config::{Appender, Root}; +use log4rs::Config; +use std::sync::Once; + +impl From for LevelFilter { + fn from(log_level: LogLevel) -> LevelFilter { + match log_level { + LogLevel::Debug => LevelFilter::Debug, + LogLevel::Info => LevelFilter::Info, + LogLevel::Warn => LevelFilter::Warn, + LogLevel::Error => LevelFilter::Error, + LogLevel::None => LevelFilter::Off, + } + } +} + +const LOG_FILE_PATH: &str = "./logs/secrets_manager_agent.log"; +const LOG_ARCHIVE_FILE_PATH_PATTERN: &str = "./logs/archive/secrets_manager_agent_{}.gz"; +const MAX_LOG_ARCHIVE_FILES: u32 = 5; +const BYTES_PER_MB: u64 = 1024 * 1024; +const MAX_ALLOWED_LOG_SIZE_IN_MB: u64 = 10; +const FILE_APPENDER: &str = "FILE_APPENDER"; + +#[doc(hidden)] +static STARTUP: Once = Once::new(); + +/// Initializes file based logging for the daemon. +/// +/// # Arguments +/// +/// * `log_level` - The log level to report. +/// +/// # Returns +/// +/// * `Ok(())` - If no errors are encountered. +/// * `Err(Error)` - For errors initializing the log. +pub fn init_logger(log_level: LogLevel) -> Result<(), Box> { + let fixed_window_roller = + FixedWindowRoller::builder().build(LOG_ARCHIVE_FILE_PATH_PATTERN, MAX_LOG_ARCHIVE_FILES)?; + let fixed_window_roller = Box::new(fixed_window_roller); + + let file_size_trigger = Box::new(SizeTrigger::new(MAX_ALLOWED_LOG_SIZE_IN_MB * BYTES_PER_MB)); + let compound_policy = Box::new(CompoundPolicy::new(file_size_trigger, fixed_window_roller)); + + let rolling_file_appender = + RollingFileAppender::builder().build(LOG_FILE_PATH, compound_policy)?; + + let log_config = Config::builder() + .appender(Appender::builder().build(FILE_APPENDER, Box::new(rolling_file_appender))) + .build( + Root::builder() + .appender(FILE_APPENDER) + .build(log_level.into()), + )?; + + // Don't initialize logging more than once in unit tests. + let mut res: Option = None; + STARTUP.call_once(|| { + if let Err(err) = log4rs::init_config(log_config) { + res = Some(err); + } + }); + if let Some(err) = res { + return Err(Box::new(err)); + } + + info!("Logger initialized at `{:?}` log level.", log_level); + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::config::LogLevel; + use crate::logging::init_logger; + use log::{debug, error, info, warn, LevelFilter}; + + /// Tests that logger is getting initialized without errors. + #[test] + fn test_init_logger() { + init_logger(LogLevel::Info).unwrap(); + debug!("{:?}", "Debug log"); + error!("{:?}", "Error log"); + info!("{:?}", "Info log"); + warn!("{:?}", "Warn log"); + } + + /// Tests that LogLevel is correctly getting converted to LevelFilter + #[test] + fn test_log_level_to_level_filter_conversion() { + assert_eq!( + >::into(LogLevel::Debug), + LevelFilter::Debug + ); + assert_eq!( + >::into(LogLevel::Info), + LevelFilter::Info + ); + assert_eq!( + >::into(LogLevel::Warn), + LevelFilter::Warn + ); + assert_eq!( + >::into(LogLevel::Error), + LevelFilter::Error + ); + assert_eq!( + >::into(LogLevel::None), + LevelFilter::Off + ); + } +} diff --git a/aws_secretsmanager_agent/src/main.rs b/aws_secretsmanager_agent/src/main.rs new file mode 100644 index 0000000..8e73a22 --- /dev/null +++ b/aws_secretsmanager_agent/src/main.rs @@ -0,0 +1,870 @@ +use log::{error, info}; +use tokio::net::TcpListener; + +use std::env; +use std::net::SocketAddr; +mod error; +mod parse; + +mod cache_manager; +mod server; +use server::Server; +mod config; +mod constants; +mod logging; +mod utils; + +use config::Config; +use constants::VERSION; +use logging::init_logger; +use utils::get_token; + +/// Main entry point for the daemon. +/// +/// # Returns +/// +/// * `Ok(())` - Never retuned. +/// * `Box>` - Retruned for errors initializing the agent. +#[tokio::main] +async fn main() -> Result<(), Box> { + run(env::args(), &report, &forever).await +} + +/// Private helper to report startup and the listener port. +/// +/// The private helper just prints the startup info. In unit tests a different +/// helper is used to report back the server port. +/// +/// # Arguments +/// +/// * `addr` - The socket address on which the daemon is listening. +/// +/// # Example +/// +/// ``` +/// use std::net::SocketAddr; +/// report( &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 2773) ); +/// ``` +#[doc(hidden)] +fn report(addr: &SocketAddr) { + let start_msg = format!( + "Agent/{} listening on http://{}", + VERSION.unwrap_or("0.0.0"), + addr + ); + println!("{start_msg}"); + info!("{start_msg}"); +} + +/// Private helper used to run the server fovever. +/// +/// This helper is used when the server is started through the main entry point. +/// In unit tests a different helper is used to signal shutdown. +/// +/// # Returns +/// +/// * bool - Always returns false so the server never shuts down. +/// +/// # Example +/// +/// ``` +/// assert_eq!(forever(), false); +/// ``` +#[doc(hidden)] +fn forever() -> bool { + false +} + +/// Private helper do the main body of the server. +/// +/// # Arguments +/// +/// * `addr` - The socket address on which the daemon is listening. +/// * `report` - A call back used to report startup and the listener port. +/// * `end` - A call back used to signal shut down. +/// +/// # Returns +/// +/// * `Ok(())` - Never retuned when started by the main entry point. +/// * `Box` - Retruned for errors initializing the agent. +#[doc(hidden)] +async fn run bool>( + args: impl IntoIterator, + mut report: S, + mut end: E, +) -> Result<(), Box> { + let (cfg, listener) = init(args).await; + let addr = listener.local_addr()?; + let svr = Server::new(listener, &cfg).await?; + + report(&addr); // Report the port used. + + // Spawn a handler for each incomming request. + loop { + // Report errors on accept. + if let Err(msg) = svr.serve_request().await { + error!("Could not accept connection: {:?}", msg); + } + + // Check for end of test in unit tests. + if end() { + return Ok(()); + } + } +} + +/// Private helper to perform initialization. +/// +/// # Arguments +/// +/// * `args` - The command line args. +/// +/// # Returns +/// +/// * (Config, TcpListener) - The configuration info and the TCP listener. +/// +/// ``` +#[doc(hidden)] +async fn init(args: impl IntoIterator) -> (Config, TcpListener) { + // Get the arg iterator and program name from arg 0. + let mut args = args.into_iter(); + let usage = format!( + "Usage: {} [--config ]", + args.next().unwrap_or_default().as_str() + ); + let usage = usage.as_str(); + let mut config_file = None; + + // Parse command line args and see if there is a config file. + while let Some(arg) = args.next() { + match arg.as_str() { + "-c" | "--config" => { + config_file = args.next().or_else(|| err_exit("Argument expected", usage)) + } + "-h" | "--help" => err_exit("", usage), + _ => err_exit(&format!("Unknown option {arg}"), usage), + } + } + + // Initialize the config options. + let config = match Config::new(config_file.as_deref()) { + Ok(conf) => conf, + Err(msg) => err_exit(&msg.to_string(), ""), + }; + + // Initialize logging + if let Err(msg) = init_logger(config.log_level()) { + err_exit(&msg.to_string(), ""); + } + + // Verify the SSRF token env variable is set + if let Err(err) = get_token(&config) { + let msg = format!( + "Could not read SSRF token variable(s) {:?}: {err}", + config.ssrf_env_variables() + ); + error!("{msg}"); + err_exit(&msg, ""); + } + + // Bind the listener to the specified port + let addr: SocketAddr = ([127, 0, 0, 1], config.http_port()).into(); + let listener: TcpListener = match TcpListener::bind(addr).await { + Ok(x) => x, + Err(err) => { + let msg = format!("Could not bind to {addr}: {}", err); + error!("{msg}"); + err_exit(&msg, ""); + } + }; + + (config, listener) +} + +/// Private helper print error messages and exit the process with an error. +/// +/// # Arguments +/// +/// * `msg` - An error message to print (or the empty string if none is to be printed). +/// * `usage` - A usage message to print (or the empty string if none is to be printed). +#[doc(hidden)] +#[cfg(not(test))] +fn err_exit(msg: &str, usage: &str) -> ! { + if !msg.is_empty() { + eprintln!("{msg}"); + } + if !usage.is_empty() { + eprintln!("{usage}"); + } + std::process::exit(1); +} +#[cfg(test)] // Use panic for testing +fn err_exit(msg: &str, usage: &str) -> ! { + if !msg.is_empty() { + panic!("{msg} !!!"); // Suffix message with !!! so we can distinguish it in tests + } + if !usage.is_empty() { + panic!("#{usage}"); // Preceed usage with # so we can distinguish it in tests. + } + panic!("Should not get here"); +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_sdk_secretsmanager as secretsmanager; + use bytes::Bytes; + use cache_manager::tests::{ + set_client, timeout_client, DEFAULT_LABEL, DEFAULT_VERSION, FAKE_ARN, + }; + use http_body_util::{BodyExt, Empty}; + use hyper::header::{HeaderName, HeaderValue}; + use hyper::{client, Request, StatusCode}; + use hyper_util::rt::TokioIo; + use serde_json::Value; + use std; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + use std::sync::{mpsc, Arc, Mutex}; + use std::time::Duration; + use std::{fs, thread}; + use tokio; + use tokio::net::TcpStream; + use tokio::task::JoinSet; + use tokio::time::timeout; + #[cfg(unix)] + use utils::test::set_test_var; // set_test_var does not work across threads (e.g. run_request) + use utils::tests::{tmpfile_name, CleanUp}; + + fn one_shot() -> bool { + true // Tell the sever to quit + } + fn noop(_addr: &SocketAddr) {} + + // Run a timer for a test that is expected to panic. + async fn panic_test(args: impl IntoIterator) { + let vargs: Vec = args.into_iter().map(String::from).collect(); + let _ = timeout(Duration::from_secs(5), async { + run(vargs, noop, one_shot).await + }) + .await + .expect("Timed out waiting for panic"); + panic!("Did not panic!"); + } + + // Helpers to run the server in the back ground and send it the given request(s). + async fn run_request(req: &str) -> (StatusCode, Bytes) { + run_requests_with_verb(vec![("GET", req)]) + .await + .expect("request failed") + .pop() + .unwrap() + } + async fn run_requests_with_verb( + req_vec: Vec<(&str, &str)>, + ) -> Result, Box> { + run_requests_with_headers(req_vec, vec![("X-Aws-Parameters-Secrets-Token", "xyzzy")]).await + } + async fn run_requests_with_headers( + req_vec: Vec<(&str, &str)>, + headers: Vec<(&str, &str)>, + ) -> Result, Box> { + run_requests_with_client(req_vec, headers, None).await + } + async fn run_timeout_request(req: &str) -> (StatusCode, Bytes) { + run_requests_with_client( + vec![("GET", req)], + vec![("X-Aws-Parameters-Secrets-Token", "xyzzy")], + Some(timeout_client()), + ) + .await + .expect("request failed") + .pop() + .unwrap() + } + async fn run_requests_with_client( + req_vec: Vec<(&str, &str)>, + headers: Vec<(&str, &str)>, + opt_client: Option, + ) -> Result, Box> { + // Run server on port 0 which tells the OS to find an open port. + let args = vec![ + String::from("prog"), + String::from("--config"), + String::from("tests/resources/configs/config_file_anyport.toml"), + ]; + let (tx_addr, rx_addr) = mpsc::channel(); // Open channel for server to report the port + let (tx_lock, rx_lock) = mpsc::channel(); // Open channel to use as a sync primitive/lock + + let end = move || { + rx_lock.recv().expect("no shutdown signal") // Wait for shutdown signal + }; + let rpt = move |addr: &SocketAddr| { + tx_addr.send(*addr).expect("could not send address"); + }; + + // Run the http server in the background and find the port it is using + let thr = thread::Builder::new().spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build() + .unwrap(); + rt.block_on(async move { + if let Some(client) = opt_client { + set_client(client); + } + run(args, rpt, end).await.expect("could not run server"); + }) + })?; + let addr = rx_addr.recv()?; + + // Run the series of requests and build up the responses. + // Each request is run as an async task so they can overlap time wise. + let mut join_set = JoinSet::new(); + let send_cnt = req_vec.len(); + let mut idx = 0; + let responses = Arc::new(Mutex::new(Vec::new())); + for (meth, query) in req_vec.clone() { + // Setup the connection to the server + let stream = TcpStream::connect(addr) + .await + .expect("could not setup client stream"); + let io = TokioIo::new(stream); + let (mut sender, conn) = client::conn::http1::handshake(io) + .await + .expect("could not setup client"); + // spawn a task to poll the connection and drive the HTTP state + tokio::spawn(async move { + if let Err(e) = conn.await { + panic!("Error in connection: {}", e); + } + }); + + // Format the request + let mut req = Request::builder() + .uri(query) + .method(meth) + .body(Empty::::new()) + .expect("could not build request"); + for (header, header_val) in headers.clone() { + req.headers_mut().insert( + HeaderName::from_lowercase(header.to_lowercase().as_bytes())?, + HeaderValue::from_str(header_val)?, + ); + } + + // Send the request and add the response to the list. + let rsp_vec = responses.clone(); + join_set.spawn(async move { + // Get the response, map IncompleteMessage error to timeout + let rsp = match sender.send_request(req).await { + Ok(x) => x, + Err(h_err) if h_err.is_incomplete_message() => { + rsp_vec.lock().expect("lock poisoned").push(( + idx, + StatusCode::GATEWAY_TIMEOUT, + Bytes::new(), + )); + return; + } + _ => panic!("unknown error sending request"), + }; + + // Return the status code and response data + let status = rsp.status(); + let data = rsp + .into_body() + .collect() + .await + .expect("can not read body") + .to_bytes(); + + rsp_vec + .lock() + .expect("lock poisoned") + .push((idx, status, data)); + }); + + // Inject an inter message delay for all but the last request + idx += 1; + if idx < send_cnt { + tx_lock.send(false).expect("could not sync"); // Tell the server to continue for all but the last request. + tokio::time::sleep(Duration::from_secs(4)).await; + } + } + + // Check for errors. + while let Some(res) = join_set.join_next().await { + res.expect("task failed"); + } + + // Make sure everything shutdown cleanly. + tx_lock.send(true).expect("could not sync"); // Tell the server to shut down. + if let Err(msg) = thr.join() { + panic!("server failed: {:?}", msg); + } + + // Return the responses in the original request order and strip out the index. + let mut rsp_vec = responses.clone().lock().expect("lock poisoned").to_vec(); + rsp_vec.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); + Ok(rsp_vec + .iter() + .map(|x| (x.1, x.2.clone())) + .collect::>()) + } + + // Private helper to validate the response fields. + fn validate_response(name: &str, body: Bytes) { + validate_response_extra(name, DEFAULT_VERSION, vec![DEFAULT_LABEL], body); + } + + // Private helper to validate the response fields. + fn validate_response_extra(name: &str, version: &str, labels: Vec<&str>, body: Bytes) { + let map: serde_json::Map = serde_json::from_slice(&body).unwrap(); + + // Validate all the fields. + let fake_arn = FAKE_ARN.replace("{{name}}", name); + assert_eq!(map.get("Name").unwrap(), name); + assert_eq!(map.get("ARN").unwrap(), &fake_arn); + assert_eq!(map.get("VersionId").unwrap(), version); + assert_eq!(map.get("SecretString").unwrap(), "hunter2"); + assert_eq!(map.get("CreatedDate").unwrap(), "1569534789.046"); + assert_eq!( + map.get("VersionStages").unwrap().as_array().unwrap(), + &labels + ); + } + + // Private helper to validate an error response. + fn validate_err(err_code: &str, msg: &str, body: Bytes) { + let map: serde_json::Map = serde_json::from_slice(&body).unwrap(); + assert_eq!(map.get("__type").unwrap(), err_code); + if !msg.is_empty() && err_code != "InternalFailure" { + assert_eq!(map.get("message").unwrap(), msg); + } + } + + // Verify the report and forever functions do not panic + #[test] + fn test_report() { + report(&SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 2773, + )); + assert!(!forever()); + } + + // Verify the correct error message for unknown options + #[tokio::test] + #[should_panic(expected = "Unknown option -failure !!!")] // Failure is not an option. + async fn unkown_arg() { + panic_test(vec!["prog", "--config", "NoSuchFile", "-failure"]).await; + } + + // Verify the correct error message when --config is specified with no argument + #[tokio::test] + #[should_panic(expected = "Argument expected !!!")] + async fn missing_arg() { + panic_test(vec!["prog", "--config"]).await; + } + + // Verify the correct message for the --help option + #[tokio::test] + #[should_panic(expected = "#Usage: prog [--config ]")] + async fn help_arg() { + panic_test(vec!["prog", "--help"]).await; + } + + // Verify the correct error is returned for non-existant config files. + #[tokio::test] + #[should_panic(expected = "configuration file \"NoSuchFile\" not found !!!")] + async fn nofile_arg() { + panic_test(vec!["prog", "-c", "NoSuchFile"]).await; + } + + // Verify the correct error is returned when the token env var is not set. + #[tokio::test] + #[should_panic( + expected = "Could not read SSRF token variable(s) [\"FAIL_TOKEN\"]: environment variable not found !!!" + )] + async fn no_token_env() { + // Generate a temp config file that uses FAIL_TOKEN which forces the unset env var behavior in unit test. + let tmpfile = tmpfile_name("no_token_env.toml"); + let _cleanup = CleanUp { + file: Some(&tmpfile), + }; + fs::write(&tmpfile, "ssrf_env_variables = [\"FAIL_TOKEN\"]").expect("could not write"); + + panic_test(vec!["prog", "-c", &tmpfile]).await; + } + + // Verify the correct error is returned when a token file can not be read. + #[cfg(unix)] + #[tokio::test] + #[should_panic( + expected = "Could not read SSRF token variable(s) [\"AWS_TOKEN\", \"AWS_SESSION_TOKEN\"]: Permission denied (os error 13) !!!" + )] + async fn bad_token_file() { + // Generate a temp file with the default token and take away read permissions. + let tmpfile = tmpfile_name("bad_token_file.toml"); + let _cleanup = CleanUp { + file: Some(&tmpfile), + }; + fs::write(&tmpfile, "xyzzy").expect("could not write"); + fs::set_permissions(&tmpfile, fs::Permissions::from_mode(0o333)) + .expect("could not set perms"); // No read permissions + let file = Box::new(format!("file://{tmpfile}")); + set_test_var("AWS_TOKEN", Box::leak(file)); + + panic_test(vec!["prog"]).await; + } + + // Verify we correctly handle port in use errors + #[tokio::test] + #[cfg_attr(unix, should_panic(expected = "Address already in use"))] + #[cfg_attr( + windows, + should_panic( + expected = "Only one usage of each socket address (protocol/network address/port) is normally permitted." + ) + )] + async fn port_in_use() { + // Generate a temp file and auto-remove it at the end of test. + let tmpfile = tmpfile_name("port_in_use.toml"); + let _cleanup = CleanUp { + file: Some(&tmpfile), + }; + + // Bind to an arbitrary port. + let addr: SocketAddr = ([127, 0, 0, 1], 0).into(); + let listener: TcpListener = TcpListener::bind(addr) + .await + .expect("Could not bind to port"); + let port = listener.local_addr().expect("can not find port").port(); + + // Write out a temp config file with the port. + fs::write(&tmpfile, format!("http_port = {port}")).expect("could not write"); + + panic_test(vec!["prog", "-c", &tmpfile]).await; + } + + // Verify a basic ping request succeeds. + #[tokio::test] + async fn ping_req() { + let (status, body) = run_request("/ping").await; + assert_eq!(status, StatusCode::OK); + assert_eq!(body, "healthy"); + } + + // Verify ping does not require a token + #[tokio::test] + async fn ping_no_token() { + let (status, body) = run_requests_with_headers(vec![("GET", "/ping")], vec![]) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::OK); + assert_eq!(body, "healthy"); + } + + // Verify unknown paths fail with 404 + #[tokio::test] + async fn pong_req() { + let (status, _) = run_request("/pong").await; + assert_eq!(status, StatusCode::NOT_FOUND); + } + + // Verify query requests return 400 when missing a secret id + #[tokio::test] + async fn missing_id() { + let (status, _) = run_request("/secretsmanager/get").await; + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + // Verify path based requests return 400 when missing a secret id + #[tokio::test] + async fn missing_path_id() { + let (status, _) = run_request("/v1/").await; + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + // Verify that query with parameter "abc" returns 400 + #[tokio::test] + async fn bad_query_parameter() { + let (status, _) = run_request( + "/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING&abc=XXXXXXXXXXXX", + ) + .await; + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + // Verify that path query with parameter "abc" returns 400 + #[tokio::test] + async fn path_bad_query_parameter() { + let (status, _) = run_request("/v1/MyTest?versionStage=AWSPENDING&abc=XXXXXXXXXXXX").await; + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + #[tokio::test] + // Verify that path query with missing parameter "secretId" returns 400 + async fn missing_query_parameter() { + let (status, _) = run_request("/secretsmanager/get?versionStage=AWSPENDING").await; + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + // Verify a basic query request succeeds + #[tokio::test] + async fn basic_success() { + let (status, body) = run_request("/secretsmanager/get?secretId=MyTest").await; + assert_eq!(status, StatusCode::OK); + validate_response("MyTest", body); + } + + // Verify a query using the pending label + #[tokio::test] + async fn pending_success() { + let req = format!("/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING"); + let (status, body) = run_request(&req).await; + assert_eq!(status, StatusCode::OK); + validate_response_extra("MyTest", DEFAULT_VERSION, vec!["AWSPENDING"], body); + } + + // Verify a query for a specific version. + #[tokio::test] + async fn version_success() { + let ver = "11111"; + let req = format!("/secretsmanager/get?secretId=MyTest&versionId={ver}"); + let (status, body) = run_request(&req).await; + assert_eq!(status, StatusCode::OK); + validate_response_extra("MyTest", ver, vec![DEFAULT_LABEL], body); + } + + // Verify a query request with all args. + #[tokio::test] + async fn all_args_success() { + let ver = "000000000000"; + let req = + format!("/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING&versionId={ver}"); + let (status, body) = run_request(&req).await; + assert_eq!(status, StatusCode::OK); + validate_response_extra("MyTest", ver, vec!["AWSPENDING"], body); + } + + // Verify access denied errors + #[tokio::test] + async fn access_denied_test() { + let (status, body) = run_request("/secretsmanager/get?secretId=KMSACCESSDENIEDTest").await; + assert_eq!(status, StatusCode::BAD_REQUEST); + validate_err( + "AccessDeniedException", + "Access to KMS is not allowed", + body, + ); + } + + // Verify creds error + #[tokio::test] + async fn other_error_test() { + let (status, body) = run_request("/secretsmanager/get?secretId=OTHERERRORTest").await; + assert_eq!(status, StatusCode::BAD_REQUEST); + validate_err( + "InvalidSignatureException", + "The request signature we calculated does not match ...", + body, + ); + } + + // Verify a basic path based request with an alternate header succeeds + #[tokio::test] + async fn path_success() { + let (status, body) = run_requests_with_headers( + vec![("GET", "/v1/MyTest")], + vec![("X-Vault-Token", "xyzzy")], + ) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::OK); + validate_response("MyTest", body); + } + + // Verify a query using the pending label + #[tokio::test] + async fn path_pending_success() { + let req = "/v1/My/Test?versionStage=AWSPENDING"; + let (status, body) = run_request(&req).await; + assert_eq!(status, StatusCode::OK); + validate_response_extra("My/Test", DEFAULT_VERSION, vec!["AWSPENDING"], body); + } + + // Verify a query for a specific version. + #[tokio::test] + async fn path_version_success() { + let ver = "11111"; + let req = format!("/v1/My/Test?versionId={ver}"); + let (status, body) = run_request(&req).await; + assert_eq!(status, StatusCode::OK); + validate_response_extra("My/Test", ver, vec![DEFAULT_LABEL], body); + } + + // Verify a query request with all args. + #[tokio::test] + async fn path_all_args_success() { + let ver = "000000000000"; + let req = format!("/v1/My/Test?versionStage=AWSPENDING&versionId={ver}"); + let (status, body) = run_request(&req).await; + assert_eq!(status, StatusCode::OK); + validate_response_extra("My/Test", ver, vec!["AWSPENDING"], body); + } + + // Verify a query request fails if the SSRF token is not present + #[tokio::test] + async fn no_token_fail() { + let (status, _) = + run_requests_with_headers(vec![("GET", "/secretsmanager/get?secretId=MyTest")], vec![]) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::FORBIDDEN); + } + + // Verify a path based request fails if the SSRF token is not present + #[tokio::test] + async fn path_no_token_fail() { + let (status, _) = run_requests_with_headers(vec![("GET", "/v1/MyTest")], vec![]) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::FORBIDDEN); + } + + // Verify failure if an incorrect token is passed. + #[tokio::test] + async fn bad_token() { + let (status, _) = run_requests_with_headers( + vec![("GET", "/secretsmanager/get?secretId=MyTest")], + vec![("X-Vault-Token", "click slipers")], + ) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::FORBIDDEN); + } + + // Verify the X-Forwarded-For header is not allowed. + #[tokio::test] + async fn xff_fail() { + let (status, _) = run_requests_with_headers( + vec![("GET", "/secretsmanager/get?secretId=MyTest")], + vec![ + ("X-Vault-Token", "xyzzy"), + ("X-Forwarded-For", "54.239.28.85"), + ], + ) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + // Verify max conn is enforced (max conn set to 1 for testing) + #[tokio::test] + async fn max_conn_test() { + /* Note that run_requests injects a 4 second inter-message delay and + * responses are returned in the orginal request order, regarless + * of timing. Also must not exceed the 10 second timeout for unit tests. + */ + let reqs = vec![ + ("GET", "/secretsmanager/get?secretId=SleepyTest_6"), // req takes 6 seconds + ("GET", "/secretsmanager/get?secretId=MyTest"), // req sent after 4 seconds + ("GET", "/secretsmanager/get?secretId=MyTest"), // req sent after 8 seconds + ]; + let mut rsp = run_requests_with_verb(reqs).await.expect("request failed"); + assert_eq!(rsp.len(), 3); // Verify 3 reponses + + // Verify the first request (the delayed request) was successful. + let (status, body) = rsp.pop().unwrap(); + assert_eq!(status, StatusCode::OK); + validate_response("SleepyTest_6", body); + + // Make sure the second request failed (because the first was still in progress) + let (status, _) = rsp.pop().unwrap(); + assert_eq!(status, StatusCode::TOO_MANY_REQUESTS); + + // Make sure the third request succeeded (because first already completed) + let (status, body) = rsp.pop().unwrap(); + assert_eq!(status, StatusCode::OK); + validate_response("MyTest", body); + } + + // Verify health checks can exceed max conn + #[tokio::test] + async fn ping_max_conn() { + let reqs = vec![ + ("GET", "/secretsmanager/get?secretId=SleepyTest_6"), // req takes 6 seconds + ("GET", "/ping"), // req sent after 4 seconds + ]; + let mut rsp = run_requests_with_verb(reqs).await.expect("request failed"); + assert_eq!(rsp.len(), 2); // Verify 2 reponses + + // Verify the first request (the delayed request) was successful. + let (status, body) = rsp.pop().unwrap(); + assert_eq!(status, StatusCode::OK); + validate_response("SleepyTest_6", body); + + // Make sure the ping was not blocked by the first request. + let (status, body) = rsp.pop().unwrap(); + assert_eq!(status, StatusCode::OK); + assert_eq!(body, "healthy"); + } + + // Verify requests time out correctly. + #[tokio::test] + async fn timeout_test() { + /* Run a request that waits forever; run_request will map + * IncompleteMessage (due to the server timeing out) to GATEWAY_TIMEOUT + */ + let (status, _) = run_timeout_request(&format!( + "/secretsmanager/get?secretId=SleepyTest_{}", + u64::MAX + )) + .await; + assert_eq!(status, StatusCode::GATEWAY_TIMEOUT); + } + + // Verify requests using the wrong verbs fail with 405. + #[tokio::test] + async fn get_only() { + for verb in [ + "POST", "PUT", "PATCH", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE", + ] { + let (status, _) = + run_requests_with_verb(vec![(verb, "/secretsmanager/get?secretId=MyTest")]) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + let (status, _) = run_requests_with_verb(vec![(verb, "/v1/MyTest")]) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + let (status, _) = run_requests_with_verb(vec![(verb, "/ping")]) + .await + .expect("request failed") + .pop() + .unwrap(); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + } + } +} diff --git a/aws_secretsmanager_agent/src/parse.rs b/aws_secretsmanager_agent/src/parse.rs new file mode 100644 index 0000000..d71cd09 --- /dev/null +++ b/aws_secretsmanager_agent/src/parse.rs @@ -0,0 +1,184 @@ +use std::borrow::Borrow; + +use url::Url; + +use crate::error::HttpError; + +#[derive(Debug)] +pub(crate) struct GSVQuery { + pub secret_id: String, + pub version_id: Option, + pub version_stage: Option, +} + +impl GSVQuery { + pub(crate) fn try_from_query(s: &str) -> Result { + // url library can only parse complete URIs. The host/port/scheme used is irrelevant since it is not used + let complete_uri = format!("http://localhost{}", s); + + let url = Url::parse(&complete_uri)?; + + let mut query = GSVQuery { + secret_id: "".into(), + version_id: None, + version_stage: None, + }; + + for (k, v) in url.query_pairs() { + match k.borrow() { + "secretId" => query.secret_id = v.into(), + "versionId" => query.version_id = Some(v.into()), + "versionStage" => query.version_stage = Some(v.into()), + p => return Err(HttpError(400, format!("unknown parameter: {}", p))), + } + } + + if query.secret_id.is_empty() { + return Err(HttpError(400, "missing parameter secretId".to_string())); + } + + Ok(query) + } + + pub(crate) fn try_from_path_query(s: &str, path_prefix: &str) -> Result { + // url library can only parse complete URIs. The host/port/scheme used is irrelevant since it gets stripped + let complete_uri = format!("http://localhost{}", s); + + let url = Url::parse(&complete_uri)?; + + let secret_id = match url.path().get(path_prefix.len()..) { + Some(s) if !s.is_empty() => s.to_string(), + _ => return Err(HttpError(400, "missing secret ID".to_string())), + }; + + let mut query = GSVQuery { + secret_id, + version_id: None, + version_stage: None, + }; + + for (k, v) in url.query_pairs() { + match k.borrow() { + "versionId" => query.version_id = Some(v.into()), + "versionStage" => query.version_stage = Some(v.into()), + p => return Err(HttpError(400, format!("unknown parameter: {}", p))), + } + } + + Ok(query) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_query() { + let secret_id = "MyTest".to_owned(); + let query = + GSVQuery::try_from_query(&format!("/secretsmanager/get?secretId={}", secret_id)) + .unwrap(); + + assert_eq!(query.secret_id, secret_id); + assert_eq!(query.version_id, None); + assert_eq!(query.version_stage, None); + } + + #[test] + fn parse_path_query() { + let secret_id = "MyTest".to_owned(); + let version_id = "myversion".to_owned(); + let version_stage = "dev".to_owned(); + let path_prefix = "/v1/"; + + let query = GSVQuery::try_from_path_query( + &format!( + "{}{}?versionId={}&versionStage={}", + path_prefix, secret_id, version_id, version_stage + ), + path_prefix, + ) + .unwrap(); + + assert_eq!(query.secret_id, secret_id); + assert_eq!(query.version_id, Some(version_id)); + assert_eq!(query.version_stage, Some(version_stage)); + } + + #[test] + fn parse_query_invalid_parameter() { + let secret_id = "MyTest".to_owned(); + let version_id = "myversion".to_owned(); + let version_stage = "dev".to_owned(); + match GSVQuery::try_from_query(&format!( + "/secretsmanager/get?secretId={}&versionId={}&versionStage={}&abc=123", + secret_id, version_id, version_stage + )) { + Ok(_) => panic!("should not parse"), + Err(e) => { + assert_eq!(e.0, 400); + assert_eq!(e.1, "unknown parameter: abc"); + } + } + } + + #[test] + fn parse_query_path_invalid_parameter() { + let secret_id = "MyTest".to_owned(); + let version_id = "myversion".to_owned(); + let version_stage = "dev".to_owned(); + let path_prefix = "/v1/"; + + match GSVQuery::try_from_path_query( + &format!( + "{}{}?versionId={}&versionStage={}&abc=123", + path_prefix, secret_id, version_id, version_stage + ), + path_prefix, + ) { + Ok(_) => panic!("should not parse"), + Err(e) => { + assert_eq!(e.0, 400); + assert_eq!(e.1, "unknown parameter: abc"); + } + } + } + + #[test] + fn parse_query_missing_secret_id() { + let version_id = "myversion".to_owned(); + let version_stage = "dev".to_owned(); + match GSVQuery::try_from_query(&format!( + "/secretsmanager/get?&versionId={}&versionStage={}", + version_id, version_stage + )) { + Ok(_) => panic!("should not parse"), + Err(e) => { + assert_eq!(e.0, 400); + assert_eq!(e.1, "missing parameter secretId"); + } + } + } + + #[test] + fn parse_query_path_missing_secret_id() { + let version_id = "myversion".to_owned(); + let version_stage = "dev".to_owned(); + let path_prefix = "/v1/"; + + match GSVQuery::try_from_path_query( + &format!( + "{}?versionId={}&versionStage={}&abc=123", + path_prefix, version_id, version_stage + ), + path_prefix, + ) { + Ok(_) => panic!("should not parse"), + Err(e) => { + assert_eq!(e.0, 400); + assert_eq!(e.1, "missing secret ID"); + } + } + } +} diff --git a/aws_secretsmanager_agent/src/server.rs b/aws_secretsmanager_agent/src/server.rs new file mode 100644 index 0000000..4c1929a --- /dev/null +++ b/aws_secretsmanager_agent/src/server.rs @@ -0,0 +1,262 @@ +use bytes::Bytes; +use http_body_util::Full; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{body::Incoming as IncomingBody, Method, Request, Response}; +use hyper_util::rt::TokioIo; +use log::error; +use tokio::net::TcpListener; +use tokio::time::timeout; + +use crate::cache_manager::CacheManager; +use crate::config::Config; +use crate::constants::MAX_BUF_BYTES; +use crate::error::HttpError; +use crate::parse::GSVQuery; +use crate::utils::{get_token, time_out}; +use std::sync::Arc; + +/// Handle incoming HTTP requests. +#[derive(Debug, Clone)] +pub struct Server { + listener: Arc, + cache_mgr: Arc, + ssrf_token: Arc, + ssrf_headers: Arc>, + path_prefix: Arc, + max_conn: usize, +} + +/// Handle incoming HTTP requests. +/// +/// Implements the HTTP handler. Each incomming request is handled in its own +/// thread. +impl Server { + /// Create a server instance. + /// + /// # Arguments + /// + /// * `listener` - The TcpListener to use to accept incomming requests. + /// * `cfg` - The config object to use for options such header names. + /// + /// # Returns + /// + /// * `Ok(Self)` - The server object. + /// * `Box>` - Retruned for errors initializing the agent + pub async fn new( + listener: TcpListener, + cfg: &Config, + ) -> Result> { + Ok(Self { + listener: Arc::new(listener), + cache_mgr: Arc::new(CacheManager::new(cfg).await?), + ssrf_token: Arc::new(get_token(cfg)?), + ssrf_headers: Arc::new(cfg.ssrf_headers()), + path_prefix: Arc::new(cfg.path_prefix()), + max_conn: cfg.max_conn(), + }) + } + + /// Accept the next request on the listener and process it in a separate thread. + /// + /// # Returns + /// + /// * `Ok(())` - The request is being handled in the background. + /// * `Err(Error)` - IOError while accepting request. + /// + /// # Errors + /// + /// * `std::io::Error` - Error while accepting request. + pub async fn serve_request(&self) -> Result<(), Box> { + let (stream, _) = self.listener.accept().await?; + stream.set_ttl(1)?; // Prohibit network hops + let io = TokioIo::new(stream); + let svr_clone = self.clone(); + let rq_cnt = Arc::strong_count(&self.cache_mgr); // concurrent request count + tokio::task::spawn(async move { + let svc_fn = service_fn(|req: Request| async { + svr_clone.complete_req(req, rq_cnt).await + }); + let mut http = http1::Builder::new(); + let http = http.max_buf_size(MAX_BUF_BYTES); + if let Err(err) = timeout(time_out(), http.serve_connection(io, svc_fn)).await { + error!("Failed to serve connection: {:?}", err); + }; + }); + + Ok(()) + } + + /// Private helper to process the incomming request body and format a response. + /// + /// # Arguments + /// + /// * `req` - The incomming HTTP request. + /// * `count` - The number of concurrent requets being handled. + /// + /// # Returns + /// + /// * `Ok(Response>)` - The HTTP response to send back. + /// * `Err(Error)` - Never returned, converted to a response. + #[doc(hidden)] + async fn complete_req( + &self, + req: Request, + count: usize, + ) -> Result>, hyper::Error> { + let result = self.get_result(&req, count).await; + + // Format the response. + match result { + Ok(rsp_body) => Ok(Response::builder() + .body(Full::new(Bytes::from(rsp_body))) + .unwrap()), + Err(e) => Ok(Response::builder() + .status(e.0) + .body(Full::new(Bytes::from(e.1))) + .unwrap()), + } + } + + /// Parse an incomming request and provide the response data. + /// + /// # Arguments + /// + /// * `req` - The incomming HTTP request. + /// * `count` - The number of concurrent requets being handled. + /// + /// # Returns + /// + /// * `Ok(String)` - The payload to return. + /// * `Err((u16, String))` - A HTTP error code and error message. + #[doc(hidden)] + async fn get_result( + &self, + req: &Request, + count: usize, + ) -> Result { + self.validate_max_conn(req, count)?; // Verify connection limits are not exceeded + self.validate_token(req)?; // Check for a valid SSRF token + self.validate_method(req)?; // Allow only GET requests + + match req.uri().path() { + "/ping" => Ok("healthy".into()), // Standard health check + + // Lambda extension style query + "/secretsmanager/get" => { + let qry = GSVQuery::try_from_query(&req.uri().to_string())?; + Ok(self + .cache_mgr + .fetch( + &qry.secret_id, + qry.version_id.as_deref(), + qry.version_stage.as_deref(), + ) + .await?) + } + + // Path style request + path if path.starts_with(self.path_prefix.as_str()) => { + let qry = GSVQuery::try_from_path_query(&req.uri().to_string(), &self.path_prefix)?; + Ok(self + .cache_mgr + .fetch( + &qry.secret_id, + qry.version_id.as_deref(), + qry.version_stage.as_deref(), + ) + .await?) + } + _ => Err(HttpError(404, "Not found".into())), + } + } + + /// Verify the incomming request does not exceed the maximum connection limit. + /// + /// The limit is not enforced for ping/health checks. + /// + /// # Arguments + /// + /// * `req` - The incomming HTTP request. + /// * `count` - The number of concurrent requets being handled. + /// + /// # Returns + /// + /// * `Ok(())` - For health checks or when the request is within limits. + /// * `Err((u16, String))` - A 429 error code and error message. + #[doc(hidden)] + fn validate_max_conn( + &self, + req: &Request, + count: usize, + ) -> Result<(), HttpError> { + // Add one to account for the extra server reference in main, allow 2 extra health check conns. + let limit = if req.uri().path() == "/ping" { + self.max_conn + 3 + } else { + self.max_conn + 1 + }; + if count <= limit { + return Ok(()); + } + + Err(HttpError(429, "Connection limit exceeded".into())) + } + + /// Verify the request has the correct SSRF token and no forwarding header is set. + /// + /// Health checks are not subject to these checks. + /// + /// # Arguments + /// + /// * `req` - The incomming HTTP request. + /// + /// # Returns + /// + /// * `Ok(String)` - The value of the secret. + /// * `Err((u16, String))` - The error code and message. + /// * `Ok(())` - For health checks or when the request has the correct token. + /// * `Err((u16, String))` - A 400 or 403 error code (if header is set or token is missing or wrong) and error message. + #[doc(hidden)] + fn validate_token(&self, req: &Request) -> Result<(), HttpError> { + if req.uri().path() == "/ping" { + return Ok(()); + } + + // Prohibit forwarding. + let headers = req.headers(); + if headers.contains_key("X-Forwarded-For") { + error!("Rejecting request with X-Forwarded-For header"); + return Err(HttpError(400, "Forwarded".into())); + } + + // Iterate through the headers looking for our token + for header in self.ssrf_headers.iter() { + if headers.contains_key(header) && headers[header] == self.ssrf_token.as_str() { + return Ok(()); + } + } + + error!("Rejecting request with incorrect SSRF token"); + Err(HttpError(403, "Bad Token".into())) + } + + /// Verify the request is using the GET HTTP verb. + /// + /// # Arguments + /// + /// * `req` - The incomming HTTP request. + /// + /// # Returns + /// + /// * `Ok(())` - If the GET verb/method is use. + /// * `Err((u16, String))` - A 405 error codde and message when GET is not used. + #[doc(hidden)] + fn validate_method(&self, req: &Request) -> Result<(), HttpError> { + if *req.method() == Method::GET { + return Ok(()); + } + + Err(HttpError(405, "Not allowed".into())) + } +} diff --git a/aws_secretsmanager_agent/src/utils.rs b/aws_secretsmanager_agent/src/utils.rs new file mode 100644 index 0000000..5c914bd --- /dev/null +++ b/aws_secretsmanager_agent/src/utils.rs @@ -0,0 +1,332 @@ +use crate::config::Config; +use crate::constants::{APPNAME, MAX_REQ_TIME_SEC, VERSION}; +use aws_sdk_secretsmanager::config::interceptors::BeforeTransmitInterceptorContextMut; +use aws_sdk_secretsmanager::config::{ConfigBag, Intercept, RuntimeComponents}; +#[cfg(not(test))] +use aws_sdk_secretsmanager::Client as SecretsManagerClient; +use std::env::VarError; +use std::fs; +use std::time::Duration; + +#[cfg(not(test))] +use std::env::var; // Use the real std::env::var +#[cfg(test)] +use tests::var_test as var; + +/// Helper to format error response body in Coral JSON 1.1 format. +/// +/// Callers need to pass in the error code (e.g. InternalFailure, +/// InvalidParameterException, ect.) and the error message. This function will +/// then format a response body in JSON 1.1 format. +/// +/// # Arguments +/// +/// * `err_code` - The modeled exception name or InternalFailure for 500s. +/// * `msg` - The optional error message or "" for InternalFailure. +/// +/// # Returns +/// +/// * `String` - The JSON 1.1 response body. +/// +/// # Example +/// +/// ``` +/// assert_eq!(err_response("InternalFailure", ""), "{\"__type\":\"InternalFailure\"}"); +/// assert_eq!( +/// err_response("ResourceNotFoundException", "Secrets Manager can't find the specified secret."), +/// "{\"__type\":\"ResourceNotFoundException\",\"message\":\"Secrets Manager can't find the specified secret.\"}" +/// ); +/// ``` +#[doc(hidden)] +pub fn err_response(err_code: &str, msg: &str) -> String { + if msg.is_empty() || err_code == "InternalFailure" { + return String::from("{\"__type\":\"InternalFailure\"}"); + } + format!("{{\"__type\":\"{err_code}\", \"message\":\"{msg}\"}}") +} + +/// Helper function to get the SSRF token value. +/// +/// Reads the SSRF token from the configured env variable. If the env variable +/// is a reference to a file (namely file://FILENAME), the data is read in from +/// that file. +/// +/// # Arguments +/// +/// * `config` - The configuration options for the daemon. +/// +/// # Returns +/// +/// * `Ok(String)` - The SSRF token value. +/// * `Err(Error)` - Error indicating that the variable is not set or could not be read. +#[doc(hidden)] +pub fn get_token(config: &Config) -> Result> { + // Iterate through the env name list looking for the first variable set + #[allow(clippy::redundant_closure)] + let found = config + .ssrf_env_variables() + .iter() + .map(|n| var(n)) + .filter_map(|r| r.ok()) + .next(); + if found.is_none() { + return Err(Box::new(VarError::NotPresent)); + } + let val = found.unwrap(); + + // If the variable is not a reference to a file, just return the value. + if !val.starts_with("file://") { + return Ok(val); + } + + // Read and return the contents of the file. + let file = val.strip_prefix("file://").unwrap(); + Ok(fs::read_to_string(file)?.trim().to_string()) +} + +#[doc(hidden)] +#[cfg(not(test))] +pub use time_out_impl as time_out; +#[cfg(test)] +pub use time_out_test as time_out; + +/// Helper function to get the time out setting for request processing. +/// +/// # Returns +/// +/// * `Durration` - How long to wait before canceling the operation. +#[doc(hidden)] +pub fn time_out_impl() -> Duration { + Duration::from_secs(MAX_REQ_TIME_SEC) +} +#[cfg(test)] +pub fn time_out_test() -> Duration { + Duration::from_secs(10) // Timeout in 10 seconds for testing. +} + +/// Validates the provided configuration and creates an AWS Secrets Manager client +/// from the latest default AWS configuration. +/// +/// # Arguments +/// +/// * `config` - A reference to a `Config` object containing the necessary configuration +/// parameters for creating the AWS Secrets Manager client. +/// +/// # Returns +/// +/// * `Ok(SecretsManagerClient)` - An AWS Secrets Manager client if the credentials are valid. +/// * `Err(Box)` if there is an error creating the Secrets Manager client +/// or validating the AWS credentials. +#[doc(hidden)] +#[cfg(not(test))] +pub async fn validate_and_create_asm_client( + config: &Config, +) -> Result> { + use aws_config::{BehaviorVersion, Region}; + + let default_config = &aws_config::load_defaults(BehaviorVersion::latest()).await; + let mut asm_builder = aws_sdk_secretsmanager::config::Builder::from(default_config) + .interceptor(AgentModifierInterceptor); + let mut sts_builder = aws_sdk_sts::config::Builder::from(default_config); + + if let Some(region) = config.region() { + asm_builder.set_region(Some(Region::new(region.clone()))); + sts_builder.set_region(Some(Region::new(region.clone()))); + } + + // Validate the region and credentials first + let sts_client = aws_sdk_sts::Client::from_conf(sts_builder.build()); + let _ = sts_client.get_caller_identity().send().await?; + + Ok(aws_sdk_secretsmanager::Client::from_conf( + asm_builder.build(), + )) +} + +/// SDK interceptor to append the agent name and version to the User-Agent header for CloudTrail records. +#[doc(hidden)] +#[derive(Debug)] +pub struct AgentModifierInterceptor; + +/// SDK interceptor to append the agent name and version to the User-Agent header for CloudTrail records. +/// +/// This interceptor adds the agent name and version to the User-Agent header +/// of outbound Secrets Manager SDK requests. +#[doc(hidden)] +impl Intercept for AgentModifierInterceptor { + fn name(&self) -> &'static str { + "AgentModifierInterceptor" + } + + fn modify_before_signing( + &self, + context: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + _cfg: &mut ConfigBag, + ) -> Result<(), aws_sdk_secretsmanager::error::BoxError> { + let request = context.request_mut(); + let agent = request.headers().get("user-agent").unwrap_or_default(); // Get current agent + let full_agent = format!("{agent} {APPNAME}/{}", VERSION.unwrap_or("0.0.0")); + request.headers_mut().insert("user-agent", full_agent); // Overwrite header. + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use std::cell::RefCell; + use std::env::temp_dir; + use std::thread_local; + use std::time::{SystemTime, UNIX_EPOCH}; + + // Used to cleanup resources after test completon. + pub struct CleanUp<'a> { + pub file: Option<&'a str>, + } + + impl Drop for CleanUp<'_> { + fn drop(&mut self) { + // Clear env var injections. + ENVVAR.set(None); + + // Cleanup temp files. + if let Some(name) = self.file { + let _ = std::fs::remove_file(name); + } + } + } + + // Create a temp file name for a test. + pub fn tmpfile_name(suffix: &str) -> String { + format!( + "{}/{}_{:?}_{suffix}", + temp_dir().display(), + std::process::id(), + SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ) + } + + // Used to inject env variable values for testing. Uses thread local data since + // multi-threaded tests setting process wide env variables can collide. + thread_local! { + static ENVVAR: RefCell>> = RefCell::new(None); + } + pub fn set_test_var(key: &'static str, val: &'static str) { + ENVVAR.set(Some(vec![(key, val)])); + } + pub fn set_test_vars(vars: Vec<(&'static str, &'static str)>) { + ENVVAR.set(Some(vars)); + } + + // Stub std::env::var that reads injected variables from thread_local + pub fn var_test(key: &str) -> Result { + // Shortcut key to force failure. + if key == "FAIL_TOKEN" { + return Err(VarError::NotPresent); + } + if let Some(varvec) = ENVVAR.with_borrow(|v| v.clone()) { + let found = varvec.iter().filter(|keyval| keyval.0 == key).next(); + if found != None { + return Ok(found.unwrap().1.to_string()); + } + } else { + // Return a default value if no value is injected. + return Ok("xyzzy".to_string()); // Poof! + } + + Err(VarError::NotPresent) // A fake value was injected but not for this key. + } + + // Verify we can read the default config variable. + #[test] + fn test_env_set() { + let _cleanup = CleanUp { file: None }; + set_test_var("AWS_TOKEN", "abc123"); + let cfg = Config::new(None).expect("config failed"); + assert_eq!(get_token(&cfg).expect("token fail"), "abc123"); + } + + // Verify we can use the second variable in the list + #[test] + fn test_alt_env_set() { + let _cleanup = CleanUp { file: None }; + set_test_var("AWS_SESSION_TOKEN", "123abc"); + let cfg = Config::new(None).expect("config failed"); + assert_eq!(get_token(&cfg).expect("token fail"), "123abc"); + } + + // Verify the variable can point to a file and we use the file contents. + #[test] + fn test_file_token() { + let token = "4 chosen by fair dice roll, guaranteed to be random"; + let tmpfile = tmpfile_name("test_file_token"); + let _cleanup = CleanUp { + file: Some(&tmpfile), + }; + std::fs::write(&tmpfile, token).expect("could not write"); + let file = Box::new(format!("file://{tmpfile}")); + set_test_var("AWS_TOKEN", Box::leak(file)); + let cfg = Config::new(None).expect("config failed"); + assert_eq!(get_token(&cfg).expect("token fail"), token); + } + + // Verify we correctly handle a missing file. + #[test] + fn test_file_token_missing() { + #[cfg(unix)] + const NO_SUCH_FILE_ERROR_MSG: &str = "No such file or directory (os error 2)"; + #[cfg(windows)] + const NO_SUCH_FILE_ERROR_MSG: &str = + "The system cannot find the file specified. (os error 2)"; + + let _cleanup = CleanUp { file: None }; + set_test_var("AWS_TOKEN", "file:///NoSuchFile"); + let cfg = Config::new(None).expect("config failed"); + assert_eq!( + get_token(&cfg).err().unwrap().to_string(), + NO_SUCH_FILE_ERROR_MSG + ); + } + + // Verify the first variable in the list takes precedence + #[test] + fn two_tokens() { + let _cleanup = CleanUp { file: None }; + set_test_vars(vec![ + ("AWS_TOKEN", "yzzyx"), + ("AWS_SESSION_TOKEN", "CTAtoken"), + ]); // Good token, unusable token. + let cfg = Config::new(None).expect("config failed"); + assert_eq!(get_token(&cfg).expect("token fail"), "yzzyx"); + } + + // Verify we return the correct error when a variable is not set. + #[test] + fn test_env_fail() { + let tmpfile = tmpfile_name("test_env_fail.toml"); + let _cleanup = CleanUp { + file: Some(&tmpfile), + }; + set_test_var("", ""); + std::fs::write(&tmpfile, format!("ssrf_env_variables = [\"NOSUCHENV\"]")) + .expect("could not write"); + let cfg = Config::new(Some(&tmpfile)).expect("config failed"); + assert_eq!( + get_token(&cfg) + .err() + .unwrap() + .downcast_ref::() + .unwrap() + .eq(&VarError::NotPresent), + true + ); + } + + // Make sure the timeout functon returns the correct value. + #[test] + fn test_time_out() { + assert_eq!(time_out_impl(), Duration::from_secs(MAX_REQ_TIME_SEC)); + } +} diff --git a/aws_secretsmanager_agent/tests/resources/configs/config_file_anyport.toml b/aws_secretsmanager_agent/tests/resources/configs/config_file_anyport.toml new file mode 100644 index 0000000..e66f0d6 --- /dev/null +++ b/aws_secretsmanager_agent/tests/resources/configs/config_file_anyport.toml @@ -0,0 +1,3 @@ +# Let the server pick the port +http_port = "0" +max_conn = 1 diff --git a/aws_secretsmanager_agent/tests/resources/configs/config_file_empty.toml b/aws_secretsmanager_agent/tests/resources/configs/config_file_empty.toml new file mode 100644 index 0000000..e69de29 diff --git a/aws_secretsmanager_agent/tests/resources/configs/config_file_valid.toml b/aws_secretsmanager_agent/tests/resources/configs/config_file_valid.toml new file mode 100644 index 0000000..b54f76b --- /dev/null +++ b/aws_secretsmanager_agent/tests/resources/configs/config_file_valid.toml @@ -0,0 +1,13 @@ +# checking that all caps for log level is accpeted. +log_level = "DEBUG" +http_port = "65535" +ssrf_headers = ["X-Aws-Parameters-Secrets-Token"] +ssrf_env_variables = ["MY_TOKEN"] +path_prefix = "/other" +# checking that number with no quotes work. +ttl_seconds = 300 +# checking that numbe with single quote works +cache_size = '1000' +max_conn = 10 +region = "us-west-2" + diff --git a/aws_secretsmanager_agent/tests/resources/configs/config_file_with_invalid_config.toml b/aws_secretsmanager_agent/tests/resources/configs/config_file_with_invalid_config.toml new file mode 100644 index 0000000..2edee69 --- /dev/null +++ b/aws_secretsmanager_agent/tests/resources/configs/config_file_with_invalid_config.toml @@ -0,0 +1,2 @@ +log_level = "an invalid log level" +http_port = "9999" \ No newline at end of file diff --git a/aws_secretsmanager_agent/tests/resources/configs/config_file_with_invalid_contents.toml b/aws_secretsmanager_agent/tests/resources/configs/config_file_with_invalid_contents.toml new file mode 100644 index 0000000..daa298a --- /dev/null +++ b/aws_secretsmanager_agent/tests/resources/configs/config_file_with_invalid_contents.toml @@ -0,0 +1 @@ +This is gibberish \ No newline at end of file diff --git a/aws_secretsmanager_agent/tests/resources/configs/config_file_with_unrecognized_override.toml b/aws_secretsmanager_agent/tests/resources/configs/config_file_with_unrecognized_override.toml new file mode 100644 index 0000000..10f6d9b --- /dev/null +++ b/aws_secretsmanager_agent/tests/resources/configs/config_file_with_unrecognized_override.toml @@ -0,0 +1,2 @@ +# unrecognized configurations +max_connections = "1001" \ No newline at end of file diff --git a/aws_secretsmanager_caching/Cargo.toml b/aws_secretsmanager_caching/Cargo.toml new file mode 100644 index 0000000..c2edd93 --- /dev/null +++ b/aws_secretsmanager_caching/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "aws_secretsmanager_caching" +version = "1.0.0" +edition = "2021" + +[dependencies] +aws-sdk-secretsmanager = "1" +aws-smithy-types = "1" +serde_json = "1" +serde_with = "3" +serde = { version = "1", features = ["derive"] } +thiserror = "1" +tokio = { version = "1", features = ["rt", "sync"] } +linked-hash-map = "0.5.6" + +[dev-dependencies] +aws-smithy-mocks-experimental = "0" +aws-smithy-runtime = { version = "1", features = ["test-util"] } +aws-sdk-secretsmanager = { version = "1", features = ["test-util"] } +tokio = { version = "1", features = ["macros", "rt", "sync"] } +http = "0" diff --git a/aws_secretsmanager_caching/src/lib.rs b/aws_secretsmanager_caching/src/lib.rs new file mode 100644 index 0000000..c4fd5f3 --- /dev/null +++ b/aws_secretsmanager_caching/src/lib.rs @@ -0,0 +1,616 @@ +// #![warn(missing_docs)] +#![warn( + missing_debug_implementations, + missing_docs, + rustdoc::missing_crate_level_docs +)] + +//! AWS Secrets Manager Caching Library + +/// Output of secret store +pub mod output; +/// Manages the lifecycle of cached secrets +pub mod secret_store; + +use aws_sdk_secretsmanager::Client as SecretsManagerClient; +use secret_store::SecretStoreError; + +use output::GetSecretValueOutputDef; +use secret_store::{MemoryStore, SecretStore}; +use std::{error::Error, num::NonZeroUsize, time::Duration}; +use tokio::sync::RwLock; + +/// AWS Secrets Manager Caching client +#[derive(Debug)] +pub struct SecretsManagerCachingClient { + /// Secrets Manager client to retrieve secrets. + asm_client: SecretsManagerClient, + /// A store used to cache secrets. + store: RwLock>, +} + +impl SecretsManagerCachingClient { + /// Create a new caching client with in-memory store + /// + /// # Arguments + /// + /// * `asm_client` - Initialized AWS SDK Secrets Manager client instance + /// * `max_size` - Maximum size of the store. + /// * `ttl` - Time-to-live of the secrets in the store. + pub fn new( + asm_client: SecretsManagerClient, + max_size: NonZeroUsize, + ttl: Duration, + ) -> Result { + Ok(Self { + asm_client, + store: RwLock::new(Box::new(MemoryStore::new(max_size, ttl))), + }) + } + + /// Retrieves the value of the secret from the specified version. + /// + /// # Arguments + /// + /// * `secret_id` - The ARN or name of the secret to retrieve. + /// * `version_id` - The version id of the secret version to retrieve. + /// * `version_stage` - The staging label of the version of the secret to retrieve. + pub async fn get_secret_value( + &self, + secret_id: &str, + version_id: Option<&str>, + version_stage: Option<&str>, + ) -> Result> { + let read_lock = self.store.read().await; + + match read_lock.get_secret_value(secret_id, version_id, version_stage) { + Ok(r) => Ok(r), + Err(SecretStoreError::ResourceNotFound) => { + drop(read_lock); + Ok(self + .refresh_secret_value(secret_id, version_id, version_stage, None) + .await?) + } + Err(SecretStoreError::CacheExpired(cached_value)) => { + drop(read_lock); + Ok(self + .refresh_secret_value(secret_id, version_id, version_stage, Some(cached_value)) + .await?) + } + Err(e) => Err(Box::new(e)), + } + } + + /// Refreshes the secret value through a GetSecretValue call to ASM + /// + /// # Arguments + /// * `secret_id` - The ARN or name of the secret to retrieve. + /// * `version_id` - The version id of the secret version to retrieve. + /// * `version_stage` - The staging label of the version of the secret to retrieve. + /// * `cached_value` - The value currently in the cache. + async fn refresh_secret_value( + &self, + secret_id: &str, + version_id: Option<&str>, + version_stage: Option<&str>, + cached_value: Option>, + ) -> Result> { + if let Some(cached_value) = cached_value { + // The cache already had a value in it, we can quick-refresh it if the value is still current. + if self + .is_current(version_id, version_stage, cached_value.clone()) + .await? + { + // Refresh the TTL by writing the same data back to the cache. + self.store.write().await.write_secret_value( + secret_id.to_owned(), + version_id.map(String::from), + version_stage.map(String::from), + *cached_value.clone(), + )?; + + return Ok(*cached_value); + } + } + + let response = self + .asm_client + .get_secret_value() + .secret_id(secret_id) + .set_version_id(version_id.map(String::from)) + .set_version_stage(version_stage.map(String::from)) + .send() + .await?; + + let result: GetSecretValueOutputDef = response.into(); + + self.store.write().await.write_secret_value( + secret_id.to_owned(), + version_id.map(String::from), + version_stage.map(String::from), + result.clone(), + )?; + + Ok(result) + } + + /// Check if the value in the cache is still fresh enough to be served again + /// + /// # Arguments + /// * `version_id` - The version id of the secret version to retrieve. + /// * `version_stage` - The staging label of the version of the secret to retrieve. Defaults to AWSCURRENT + /// * `cached_value` - The value currently in the cache. + /// + /// # Returns + /// * true if value can be reused, false if not + async fn is_current( + &self, + version_id: Option<&str>, + version_stage: Option<&str>, + cached_value: Box, + ) -> Result> { + let describe = self + .asm_client + .describe_secret() + .secret_id(cached_value.arn.unwrap()) + .send() + .await?; + + let real_vids_to_stages = match describe.version_ids_to_stages() { + Some(vids_to_stages) => vids_to_stages, + // Secret has no version Ids + None => return Ok(false), + }; + + let cached_version_id = &cached_value.version_id.clone().unwrap(); + + if let Some(version_id) = version_id { + // If we are requesting the same version id already in the cache, and that version ID still exists in AWS Secrets Manager + // then the value is current + let version_ids_match = version_id.eq(cached_version_id); + + // If a version stage was requested, check that it matches the one in the cache + let version_stages_match = match version_stage { + Some(version_stage) => match cached_value.version_stages { + Some(version_stages) => version_stages.contains(&version_stage.to_owned()), + // Version stage parameter was requested but was not found in the cache, forward request to AWS Secrets Manager + None => false, + }, + // No version stage requested, we don't need to check that it's valid + None => true, + }; + + return Ok(version_ids_match && version_stages_match); + } + + // Version id parameter was not specified + + // If no version stage was passed, check AWSCURRENT + let version_stage = match version_stage { + Some(v) => v.to_owned(), + None => "AWSCURRENT".to_owned(), + }; + + if let Some(cached_stages) = cached_value.version_stages { + // IF The desired label matches the one in the cache + if cached_stages.contains(&version_stage) + // AND version ids to stages in AWS Secrets Manager contains the version label + && real_vids_to_stages + .iter() + // AND the version id in AWS Secrets Manager already matches the version id in the cache + .any(|(k, v)| k.eq(cached_version_id) && v.contains(&version_stage)) + { + return Ok(true); + } + } + + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use tokio::time::sleep; + + use super::*; + + fn fake_client(ttl: Option) -> SecretsManagerCachingClient { + SecretsManagerCachingClient::new( + asm_mock::def_fake_client(), + NonZeroUsize::new(1000).unwrap(), + match ttl { + Some(ttl) => ttl, + None => Duration::from_secs(1000), + }, + ) + .expect("client should create") + } + + #[tokio::test] + async fn test_get_secret_value() { + let client = fake_client(None); + let secret_id = "test_secret"; + + let response = client + .get_secret_value(secret_id, None, None) + .await + .unwrap(); + + assert_eq!(response.name, Some(secret_id.to_string())); + assert_eq!(response.secret_string, Some("hunter2".to_string())); + assert_eq!( + response.arn, + Some( + asm_mock::FAKE_ARN + .replace("{{name}}", secret_id) + .to_string() + ) + ); + assert_eq!( + response.version_stages, + Some(vec!["AWSCURRENT".to_string()]) + ); + } + + #[tokio::test] + async fn test_get_secret_value_version_id() { + let client = fake_client(None); + let secret_id = "test_secret"; + let version_id = "test_version"; + + let response = client + .get_secret_value(secret_id, Some(version_id), None) + .await + .unwrap(); + + assert_eq!(response.name, Some(secret_id.to_string())); + assert_eq!(response.secret_string, Some("hunter2".to_string())); + assert_eq!(response.version_id, Some(version_id.to_string())); + assert_eq!( + response.arn, + Some( + asm_mock::FAKE_ARN + .replace("{{name}}", secret_id) + .to_string() + ) + ); + assert_eq!( + response.version_stages, + Some(vec!["AWSCURRENT".to_string()]) + ); + } + + #[tokio::test] + async fn test_get_secret_value_version_stage() { + let client = fake_client(None); + let secret_id = "test_secret"; + let stage_label = "STAGEHERE"; + + let response = client + .get_secret_value(secret_id, None, Some(stage_label)) + .await + .unwrap(); + + assert_eq!(response.name, Some(secret_id.to_string())); + assert_eq!(response.secret_string, Some("hunter2".to_string())); + assert_eq!( + response.arn, + Some( + asm_mock::FAKE_ARN + .replace("{{name}}", secret_id) + .to_string() + ) + ); + assert_eq!(response.version_stages, Some(vec![stage_label.to_string()])); + } + + #[tokio::test] + async fn test_get_secret_value_version_id_and_stage() { + let client = fake_client(None); + let secret_id = "test_secret"; + let version_id = "test_version"; + let stage_label = "STAGEHERE"; + + let response = client + .get_secret_value(secret_id, Some(version_id), Some(stage_label)) + .await + .unwrap(); + + assert_eq!(response.name, Some(secret_id.to_string())); + assert_eq!(response.secret_string, Some("hunter2".to_string())); + assert_eq!(response.version_id, Some(version_id.to_string())); + assert_eq!( + response.arn, + Some( + asm_mock::FAKE_ARN + .replace("{{name}}", secret_id) + .to_string() + ) + ); + assert_eq!(response.version_stages, Some(vec![stage_label.to_string()])); + } + + #[tokio::test] + async fn test_get_cache_expired() { + let client = fake_client(Some(Duration::from_secs(0))); + let secret_id = "test_secret"; + + // Run through this twice to test the cache expiration + for i in 0..2 { + let response = client + .get_secret_value(secret_id, None, None) + .await + .unwrap(); + + assert_eq!(response.name, Some(secret_id.to_string())); + assert_eq!(response.secret_string, Some("hunter2".to_string())); + assert_eq!( + response.arn, + Some( + asm_mock::FAKE_ARN + .replace("{{name}}", secret_id) + .to_string() + ) + ); + assert_eq!( + response.version_stages, + Some(vec!["AWSCURRENT".to_string()]) + ); + // let the entry expire + if i == 0 { + sleep(Duration::from_millis(50)).await; + } + } + } + + #[tokio::test] + #[should_panic] + async fn test_get_secret_value_kms_access_denied() { + let client = fake_client(None); + let secret_id = "KMSACCESSDENIEDabcdef"; + + client + .get_secret_value(secret_id, None, None) + .await + .unwrap(); + } + + #[tokio::test] + #[should_panic] + async fn test_get_secret_value_resource_not_found() { + let client = fake_client(None); + let secret_id = "NOTFOUNDfasefasef"; + + client + .get_secret_value(secret_id, None, None) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_is_current_default_succeeds() { + let client = fake_client(Some(Duration::from_secs(0))); + let secret_id = "test_secret"; + + let res1 = client + .get_secret_value(secret_id, None, None) + .await + .unwrap(); + + sleep(Duration::from_millis(10)).await; + + let res2 = client + .get_secret_value(secret_id, None, None) + .await + .unwrap(); + + assert_eq!(res1, res2) + } + + #[tokio::test] + async fn test_is_current_version_id_succeeds() { + let client = fake_client(Some(Duration::from_secs(0))); + let secret_id = "test_secret"; + let version_id = Some("test_version"); + + let res1 = client + .get_secret_value(secret_id, version_id, None) + .await + .unwrap(); + + sleep(Duration::from_millis(10)).await; + + let res2 = client + .get_secret_value(secret_id, version_id, None) + .await + .unwrap(); + + assert_eq!(res1, res2) + } + + #[tokio::test] + async fn test_is_current_version_stage_succeeds() { + let client = fake_client(Some(Duration::from_secs(0))); + let secret_id = "test_secret"; + let version_stage = Some("VERSIONSTAGE"); + + let res1 = client + .get_secret_value(secret_id, None, version_stage) + .await + .unwrap(); + + sleep(Duration::from_millis(10)).await; + + let res2 = client + .get_secret_value(secret_id, None, version_stage) + .await + .unwrap(); + + assert_eq!(res1, res2) + } + + #[tokio::test] + async fn test_is_current_both_version_id_and_version_stage_succeeds() { + let client = fake_client(Some(Duration::from_secs(0))); + let secret_id = "test_secret"; + let version_id = Some("test_version"); + let version_stage = Some("VERSIONSTAGE"); + + let res1 = client + .get_secret_value(secret_id, version_id, version_stage) + .await + .unwrap(); + + sleep(Duration::from_millis(10)).await; + + let res2 = client + .get_secret_value(secret_id, version_id, version_stage) + .await + .unwrap(); + + assert_eq!(res1, res2) + } + + #[tokio::test] + async fn test_is_current_describe_access_denied_fails() { + let client = fake_client(Some(Duration::from_secs(0))); + let secret_id = "DESCRIBEACCESSDENIED_test_secret"; + let version_id = Some("test_version"); + + client + .get_secret_value(secret_id, version_id, None) + .await + .unwrap(); + + sleep(Duration::from_millis(10)).await; + + match client.get_secret_value(secret_id, version_id, None).await { + Ok(_) => panic!("Expected failure"), + Err(_) => (), + } + } + + mod asm_mock { + use aws_sdk_secretsmanager as secretsmanager; + use aws_smithy_runtime::client::http::test_util::infallible_client_fn; + use aws_smithy_types::body::SdkBody; + use http::{Request, Response}; + use secretsmanager::config::BehaviorVersion; + use serde_json::Value; + + pub const FAKE_ARN: &str = + "arn:aws:secretsmanager:us-west-2:123456789012:secret:{{name}}-NhBWsc"; + pub const DEFAULT_VERSION: &str = "5767290c-d089-49ed-b97c-17086f8c9d79"; + pub const DEFAULT_LABEL: &str = "AWSCURRENT"; + + // Template GetSecretValue responses for testing + const GSV_BODY: &str = r###"{ + "ARN": "{{arn}}", + "Name": "{{name}}", + "VersionId": "{{version}}", + "SecretString": "hunter2", + "VersionStages": [ + "{{label}}" + ], + "CreatedDate": 1569534789.046 + }"###; + + // Template DescribeSecret responses for testing + const DESC_BODY: &str = r###"{ + "ARN": "{{arn}}", + "Name": "{{name}}", + "Description": "My test secret", + "KmsKeyId": "arn:aws:kms:us-west-2:123456789012:key/exampled-90ab-cdef-fedc-bbd6-7e6f303ac933", + "LastChangedDate": 1523477145.729, + "LastAccessedDate": 1524572133.25, + "VersionIdsToStages": { + "{{version}}": [ + "{{label}}" + ] + }, + "CreatedDate": 1569534789.046 + }"###; + + // Template for access denied testing + const KMS_ACCESS_DENIED_BODY: &str = r###"{ + "__type":"AccessDeniedException", + "Message":"Access to KMS is not allowed" + }"###; + + // Template for testing resource not found with DescribeSecret + const NOT_FOUND_EXCEPTION_BODY: &str = r###"{ + "__type":"ResourceNotFoundException", + "message":"Secrets Manager can't find the specified secret." + }"###; + + const SECRETSMANAGER_ACCESS_DENIED_BODY: &str = r###"{ + "__type:"AccessDeniedException", + "Message": "is not authorized to perform: secretsmanager:DescribeSecret on resource: XXXXXXXX" + }"###; + + // Private helper to look at the request and provide the correct reponse. + fn format_rsp(req: Request) -> (u16, String) { + let (parts, body) = req.into_parts(); + + let req_map: serde_json::Map = + serde_json::from_slice(body.bytes().unwrap()).unwrap(); + let version = req_map + .get("VersionId") + .map_or(DEFAULT_VERSION, |x| x.as_str().unwrap()); + let label = req_map + .get("VersionStage") + .map_or(DEFAULT_LABEL, |x| x.as_str().unwrap()); + let name = req_map.get("SecretId").unwrap().as_str().unwrap(); // Does not handle full ARN case. + + let (code, template) = match parts.headers["x-amz-target"].to_str().unwrap() { + "secretsmanager.GetSecretValue" if name.starts_with("KMSACCESSDENIED") => { + (400, KMS_ACCESS_DENIED_BODY) + } + "secretsmanager.GetSecretValue" if name.starts_with("NOTFOUND") => { + (400, NOT_FOUND_EXCEPTION_BODY) + } + "secretsmanager.GetSecretValue" => (200, GSV_BODY), + "secretsmanager.DescribeSecret" if name.contains("DESCRIBEACCESSDENIED") => { + (400, SECRETSMANAGER_ACCESS_DENIED_BODY) + } + "secretsmanager.DescribeSecret" => (200, DESC_BODY), + _ => panic!("Unknown operation"), + }; + + // Fill in the template and return the response. + let rsp = template + .replace("{{arn}}", FAKE_ARN) + .replace("{{name}}", name) + .replace("{{version}}", version) + .replace("{{label}}", label); + (code, rsp) + } + + // Test client that stubs off network call and provides a canned response. + pub fn def_fake_client() -> secretsmanager::Client { + let fake_creds = secretsmanager::config::Credentials::new( + "AKIDTESTKEY", + "astestsecretkey", + Some("atestsessiontoken".to_string()), + None, + "", + ); + let http_client = infallible_client_fn(|_req| { + let (code, rsp) = format_rsp(_req); + Response::builder() + .status(code) + .body(SdkBody::from(rsp)) + .unwrap() + }); + + secretsmanager::Client::from_conf( + secretsmanager::Config::builder() + .behavior_version(BehaviorVersion::latest()) + .credentials_provider(fake_creds) + .region(secretsmanager::config::Region::new("us-west-2")) + .http_client(http_client) + .build(), + ) + } + } +} diff --git a/aws_secretsmanager_caching/src/output.rs b/aws_secretsmanager_caching/src/output.rs new file mode 100644 index 0000000..a8ecab4 --- /dev/null +++ b/aws_secretsmanager_caching/src/output.rs @@ -0,0 +1,135 @@ +use aws_sdk_secretsmanager::operation::get_secret_value::GetSecretValueOutput; +use aws_smithy_types::base64; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_with::{serde_as, DeserializeAs, SerializeAs, TimestampSecondsWithFrac}; +use std::convert::TryFrom; +use std::time::SystemTime; + +/// Exhaustive structure to store the secret value +/// +/// We tried to De/Serialize the remote types using but couldn't as the remote types are non_exhaustive +/// This is a Rust limitation; for logical explaination see +/// We can remove this when aws sdk implements De/Serialize trait for the types. +#[serde_as] +#[derive(::std::clone::Clone, ::std::cmp::PartialEq, Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "PascalCase")] +pub struct GetSecretValueOutputDef { + /// The ARN of the secret. + #[serde(rename(serialize = "ARN"))] + pub arn: std::option::Option, + + /// The friendly name of the secret. + pub name: std::option::Option, + + /// The unique identifier of this version of the secret. + pub version_id: std::option::Option, + + /// The decrypted secret value, if the secret value was originally provided as a string or through the Secrets Manager console. + /// If this secret was created by using the console, then Secrets Manager stores the information as a JSON structure of key/value pairs. + #[serde(skip_serializing_if = "Option::is_none")] + pub secret_string: std::option::Option, + + /// Decrypted secret binary, if present. + #[serde(skip_serializing_if = "Option::is_none")] + pub secret_binary: std::option::Option, + + /// A list of all of the staging labels currently attached to this version of the secret. + #[serde(skip_serializing_if = "Option::is_none")] + pub version_stages: std::option::Option>, + + /// The date and time that this version of the secret was created. If you don't specify which version in VersionId or VersionStage, then Secrets Manager uses the AWSCURRENT version. + #[serde_as(as = "Option>")] + pub created_date: std::option::Option, +} + +impl GetSecretValueOutputDef { + /// Converts GetSecretValueOutput to GetSecretValueOutputDef + pub fn new(input: GetSecretValueOutput) -> Self { + Self { + arn: input.arn().map(|e| e.to_string()), + name: input.name().map(|e| e.to_string()), + version_id: input.version_id().map(|e| e.to_string()), + secret_string: input.secret_string().map(|e| e.to_string()), + secret_binary: input + .secret_binary() + .map(|e| BlobDef::new(e.clone().into_inner())), + created_date: input + .created_date() + .and_then(|x| SystemTime::try_from(*x).ok()), + version_stages: input.version_stages, + } + } +} + +impl From for GetSecretValueOutputDef { + fn from(input: GetSecretValueOutput) -> Self { + Self::new(input) + } +} + +/// Copy of the remote AWS SDK Blob type. +#[serde_as] +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Deserialize)] +pub struct BlobDef { + /// Binary content + pub inner: Vec, +} + +impl BlobDef { + /// Creates a new blob from the given `input`. + pub fn new(input: Vec) -> Self { + BlobDef { inner: input } + } + + /// Consumes the `Blob` and returns a `Vec` with its contents. + pub fn into_inner(self) -> Vec { + self.inner + } +} + +impl Serialize for BlobDef { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&base64::encode(self.clone().into_inner())) + } +} + +/// Copy of the remote aws_smithy_types::DateTime type. +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +#[serde(remote = "::aws_smithy_types::DateTime")] +pub struct DateTimeDef { + #[serde(getter = "::aws_smithy_types::DateTime::secs")] + seconds: i64, + #[serde(getter = "::aws_smithy_types::DateTime::subsec_nanos")] + subsecond_nanos: u32, +} + +impl SerializeAs<::aws_smithy_types::DateTime> for DateTimeDef { + fn serialize_as( + source: &::aws_smithy_types::DateTime, + serializer: S, + ) -> Result + where + S: Serializer, + { + DateTimeDef::serialize(source, serializer) + } +} + +impl<'de> DeserializeAs<'de, ::aws_smithy_types::DateTime> for DateTimeDef { + fn deserialize_as(deserializer: D) -> Result<::aws_smithy_types::DateTime, D::Error> + where + D: Deserializer<'de>, + { + DateTimeDef::deserialize(deserializer) + } +} + +impl From for ::aws_smithy_types::DateTime { + fn from(def: DateTimeDef) -> ::aws_smithy_types::DateTime { + ::aws_smithy_types::DateTime::from_secs_and_nanos(def.seconds, def.subsecond_nanos) + } +} diff --git a/aws_secretsmanager_caching/src/secret_store/memory_store/cache.rs b/aws_secretsmanager_caching/src/secret_store/memory_store/cache.rs new file mode 100644 index 0000000..02b4bea --- /dev/null +++ b/aws_secretsmanager_caching/src/secret_store/memory_store/cache.rs @@ -0,0 +1,116 @@ +use linked_hash_map::LinkedHashMap; +use std::{borrow::Borrow, hash::Hash, num::NonZeroUsize}; + +#[derive(Debug, Clone)] +/// Keeps track of the most recently used items and evicts old entries when max_size is reached +pub struct Cache { + entries: LinkedHashMap, + max_size: NonZeroUsize, +} + +/// Create a cache with a default size of 1000 +impl Default for Cache { + fn default() -> Self { + Self::new(NonZeroUsize::new(1000).unwrap()) + } +} + +impl Cache { + /// Returns a new LRUCache with default configuration. + pub fn new(max_size: NonZeroUsize) -> Self { + Cache { + entries: LinkedHashMap::new(), + max_size, + } + } + + /// Returns the number of items currently in the cache. + pub fn len(&mut self) -> usize { + self.entries.len() + } + + /// Inserts a key into the cache. + /// If the key already exists, it overwrites it + /// If the insert results in too many keys in the cache, the LRU item is removed. + pub fn insert(&mut self, key: K, val: V) { + self.entries.insert(key, val); + if self.len() > self.max_size.get() { + self.entries.pop_front(); + } + } + + /// Retrieves the key from the cache. + pub fn get(&self, key: &Q) -> Option<&V> + where + // This Q is used to allow for syntactic sugar with types like String, allowing &str as a key for example + Q: ?Sized + Hash + Eq, + K: Borrow, + { + self.entries.get(key) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type TestCache = Cache; + type TestIntCache = Cache; + + #[derive(PartialEq, Eq, Hash)] + pub struct TestCacheItem { + pub key: String, + } + + #[test] + fn len_counts() { + let mut cache = TestCache::default(); + let item = TestCacheItem { + key: "test".to_string(), + }; + assert_eq!(cache.len(), 0); + cache.insert("Test".to_string(), item); + assert_eq!(cache.len(), 1); + } + + #[test] + fn insert_inserts() { + let mut cache = TestCache::default(); + let item = TestCacheItem { + key: "test".to_string(), + }; + assert_eq!(cache.len(), 0); + cache.insert("Test".to_string(), item); + let item2 = cache.get("Test"); + assert_eq!("test", item2.unwrap().key); + } + + #[test] + fn max_limit_followed() { + let mut cache = TestIntCache::new(NonZeroUsize::new(4).unwrap()); + + cache.insert("test1".to_string(), 1); + cache.insert("test2".to_string(), 2); + cache.insert("test3".to_string(), 3); + cache.insert("test4".to_string(), 4); + assert_eq!(cache.len(), 4); + let items: Vec = cache.entries.iter().map(|t| (*t.1)).collect(); + assert_eq!(items, [1, 2, 3, 4]); + + cache.insert("test5".to_string(), 5); + assert_eq!(cache.len(), 4); + let items: Vec = cache.entries.iter().map(|t| (*t.1)).collect(); + assert_eq!(items, [2, 3, 4, 5]); + } + + #[test] + fn same_key_takes_latest_value() { + let mut cache = TestIntCache::new(NonZeroUsize::new(4).unwrap()); + + cache.insert("test1".to_string(), 1); + cache.insert("test1".to_string(), 2); + assert_eq!(cache.len(), 1); + let items: Vec = cache.entries.iter().map(|t| (*t.1)).collect(); + assert_eq!(items, [2]); + } +} diff --git a/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs b/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs new file mode 100644 index 0000000..df405dd --- /dev/null +++ b/aws_secretsmanager_caching/src/secret_store/memory_store/mod.rs @@ -0,0 +1,278 @@ +mod cache; + +use crate::output::GetSecretValueOutputDef; + +use self::cache::Cache; + +use super::{SecretStore, SecretStoreError}; + +use std::{ + num::NonZeroUsize, + time::{Duration, Instant}, +}; + +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +struct Key { + secret_id: String, + version_id: Option, + version_stage: Option, +} + +#[derive(Debug, Clone)] +struct GSVValue { + value: GetSecretValueOutputDef, + last_retrieved_at: Instant, +} + +impl GSVValue { + fn new(value: GetSecretValueOutputDef) -> Self { + Self { + value, + last_retrieved_at: Instant::now(), + } + } +} + +#[derive(Debug, Clone)] +/// In-memory secret store using an time and space bound cache +pub struct MemoryStore { + gsv_cache: Cache, + ttl: Duration, +} + +impl Default for MemoryStore { + fn default() -> Self { + Self::new(NonZeroUsize::new(1000).unwrap(), Duration::from_secs(60)) + } +} + +impl MemoryStore { + /// Create a new memory store with the given max size and TTL + pub fn new(max_size: NonZeroUsize, ttl: Duration) -> Self { + Self { + gsv_cache: Cache::new(max_size), + ttl, + } + } +} + +impl SecretStore for MemoryStore { + fn get_secret_value( + &self, + secret_id: &str, + version_id: Option<&str>, + version_stage: Option<&str>, + ) -> Result { + match self.gsv_cache.get(&Key { + secret_id: secret_id.to_string(), + version_id: version_id.map(String::from), + version_stage: version_stage.map(String::from), + }) { + Some(gsv) if gsv.last_retrieved_at.elapsed() > self.ttl => { + Err(SecretStoreError::CacheExpired(Box::new(gsv.value.clone()))) + } + Some(gsv) => Ok(gsv.clone().value), + None => Err(SecretStoreError::ResourceNotFound), + } + } + + fn write_secret_value( + &mut self, + secret_id: String, + version_id: Option, + version_stage: Option, + data: GetSecretValueOutputDef, + ) -> Result<(), SecretStoreError> { + self.gsv_cache.insert( + Key { + secret_id: secret_id.to_string(), + version_id, + version_stage, + }, + GSVValue::new(data), + ); + + Ok(()) + } +} + +/// Write the secret value to the store + +#[cfg(test)] +mod tests { + + use core::panic; + use std::thread; + + use crate::output::GetSecretValueOutputDef; + + use super::*; + + const NAME: &str = "test_name"; + const ARN: &str = "test_arn"; + const VERSION_ID: &str = "test_version_id"; + const SECRET_STRING: &str = "test_secret_string"; + + fn get_secret_value_output(suffix: Option<&str>) -> GetSecretValueOutputDef { + GetSecretValueOutputDef { + name: match suffix { + Some(suffix) => Some(format!("{}{}", NAME, suffix)), + None => Some(NAME.to_string()), + }, + arn: match suffix { + Some(suffix) => Some(format!("{}{}", ARN, suffix)), + None => Some(ARN.to_string()), + }, + version_id: Some(VERSION_ID.to_string()), + secret_string: Some(SECRET_STRING.to_string()), + secret_binary: None, + version_stages: Some(vec!["AWSCURRENT".to_string()]), + created_date: None, + } + } + + fn store_secret( + store: &mut MemoryStore, + suffix: Option<&str>, + version_id: Option, + stage: Option, + ) { + let name = match suffix { + Some(suffix) => format!("{}{}", NAME, suffix), + None => NAME.to_string(), + }; + + store + .write_secret_value(name, version_id, stage, get_secret_value_output(None)) + .unwrap(); + } + + #[test] + fn memory_store_write_then_read_awscurrent() { + let mut store = MemoryStore::default(); + + store_secret(&mut store, None, None, None); + + match store.get_secret_value(NAME, None, None) { + Ok(gsv) => { + assert_eq!(gsv.name.unwrap(), NAME); + assert_eq!(gsv.arn.unwrap(), ARN); + assert_eq!(gsv.version_id.unwrap(), VERSION_ID); + assert_eq!(gsv.secret_string.unwrap(), SECRET_STRING); + assert_eq!(gsv.version_stages.unwrap().len(), 1); + assert_eq!(gsv.created_date, None); + } + Err(e) => panic!("Unexpected error: {}", e), + } + } + + #[test] + fn memory_store_write_then_read_specific_stage() { + let mut store = MemoryStore::default(); + + store_secret(&mut store, None, None, Some("AWSCURRENT".to_string())); + + match store.get_secret_value(NAME, None, Some("AWSCURRENT")) { + Ok(gsv) => { + assert_eq!(gsv.name.unwrap(), NAME); + assert_eq!(gsv.arn.unwrap(), ARN); + assert_eq!(gsv.version_id.unwrap(), VERSION_ID); + assert_eq!(gsv.secret_string.unwrap(), SECRET_STRING); + assert_eq!(gsv.version_stages.unwrap().len(), 1); + assert_eq!(gsv.created_date, None); + } + Err(e) => panic!("Unexpected error: {}", e), + } + } + + #[test] + fn memory_store_write_then_read_specific_version_id() { + let mut store = MemoryStore::default(); + + store_secret(&mut store, None, Some(VERSION_ID.to_string()), None); + + let gsv = store + .get_secret_value(NAME, Some(VERSION_ID), None) + .unwrap(); + + assert_eq!(gsv.name.unwrap(), NAME); + assert_eq!(gsv.arn.unwrap(), ARN); + assert_eq!(gsv.version_id, Some(VERSION_ID.to_string())); + assert_eq!(gsv.secret_string, Some(SECRET_STRING.to_string())); + assert_eq!(gsv.version_stages, Some(vec!["AWSCURRENT".to_string()])); + assert_eq!(gsv.created_date, None); + } + + #[test] + fn memory_store_read_cache_expired() { + // Set TTL to 1ms to invalidate results right after GSV retrieval to store secret value + let mut store = MemoryStore::new(NonZeroUsize::new(10).unwrap(), Duration::from_millis(0)); + + store_secret(&mut store, None, None, None); + + thread::sleep(Duration::from_millis(1)); + + let secret_value_output_read = store.get_secret_value(NAME, None, None); + + match secret_value_output_read { + Err(SecretStoreError::CacheExpired(_)) => (), + _ => panic!("Unexpected error"), + } + } + + #[test] + fn memory_store_evicts_on_max_size() { + let mut store = MemoryStore::new(NonZeroUsize::new(1).unwrap(), Duration::from_secs(1000)); + + // Write a secret + store_secret(&mut store, None, None, None); + + // Write a second secret + store_secret(&mut store, Some("2"), None, None); + + let secret_value_output_read = store.get_secret_value(NAME, None, None); + + match secret_value_output_read { + Err(SecretStoreError::ResourceNotFound) => (), + Ok(r) => panic!("Unexpected value {:?}", r), + Err(e) => panic!("Unexpected error: {}", e), + } + + let second_secret_read = + store.get_secret_value(format!("{}{}", NAME, "2").as_str(), None, None); + + if let Err(e) = second_secret_read { + panic!("Unexpected error: {}", e) + } + } + + #[test] + fn memory_store_read_both_version_id_and_stage_succeeds() { + let mut store = MemoryStore::default(); + + store_secret( + &mut store, + None, + Some(VERSION_ID.to_string()), + Some("AWSCURRENT".to_string()), + ); + + match store.get_secret_value(NAME, Some(VERSION_ID), Some("AWSCURRENT")) { + Ok(_) => (), + Err(e) => panic!("Unexpected error: {}", e), + } + } + + #[test] + fn memory_store_read_non_existent_version_stage_fails() { + let mut store = MemoryStore::default(); + + store_secret(&mut store, None, None, None); + + match store.get_secret_value(NAME, None, Some("NONEXISTENTSTAGE")) { + Err(SecretStoreError::ResourceNotFound) => (), + Ok(r) => panic!("Expected error, got {:?}", r), + Err(e) => panic!("Unexpected error: {}", e), + } + } +} diff --git a/aws_secretsmanager_caching/src/secret_store/mod.rs b/aws_secretsmanager_caching/src/secret_store/mod.rs new file mode 100644 index 0000000..f566727 --- /dev/null +++ b/aws_secretsmanager_caching/src/secret_store/mod.rs @@ -0,0 +1,48 @@ +mod memory_store; + +pub use memory_store::MemoryStore; +use serde::{Deserialize, Serialize}; +use std::{error::Error, fmt::Debug}; + +use crate::output::GetSecretValueOutputDef; + +/// Response of the GetSecretValue API +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetSecretValueOutput(pub GetSecretValueOutputDef); + +/// SecretStore trait +/// Any struct that implements this trait can be used as a secret store. +pub trait SecretStore: Debug + Send + Sync { + /// Get the secret value from the store + fn get_secret_value<'a>( + &'a self, + secret_id: &'a str, + version_id: Option<&'a str>, + version_stage: Option<&'a str>, + ) -> Result; + + /// Write the secret value to the store + fn write_secret_value( + &mut self, + secret_id: String, + version_id: Option, + version_stage: Option, + data: GetSecretValueOutputDef, + ) -> Result<(), SecretStoreError>; +} + +/// All possible error types +#[derive(thiserror::Error, Debug)] +pub enum SecretStoreError { + /// Secret not found + #[error("Secrets Manager can't find the specified secret.")] + ResourceNotFound, + + /// Secret cache TTL expired + #[error("cache expired")] + CacheExpired(Box), + + /// An unexpected error occurred + #[error("unhandled error {0:?}")] + Unhandled(#[source] Box), +}