From bcbd755a37bc7f783487b0d8ad3be194b82e3355 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 12 Sep 2025 18:36:16 -0600 Subject: [PATCH] feat: GCP Secret Manager support --- Cargo.lock | 583 ++++++++++++++++++++++++++-- Cargo.toml | 2 + src/bin/gman/cli.rs | 10 +- src/config.rs | 4 + src/providers/gcp_secret_manager.rs | 201 ++++++++++ src/providers/mod.rs | 12 +- 6 files changed, 768 insertions(+), 44 deletions(-) create mode 100644 src/providers/gcp_secret_manager.rs diff --git a/Cargo.lock b/Cargo.lock index b4801d9..a12371e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,19 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-compression" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -537,6 +550,51 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -589,7 +647,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -674,9 +732,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.36" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ "find-msvc-tools", "jobserver", @@ -824,6 +882,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compression-codecs" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + [[package]] name = "confy" version = "1.0.0" @@ -893,6 +968,24 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossterm" version = "0.29.0" @@ -1127,12 +1220,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1147,6 +1240,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.10.0" @@ -1266,6 +1369,34 @@ dependencies = [ "slab", ] +[[package]] +name = "gcloud-sdk" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dcccf7c0cc0986cb5f476854a5c63b95bab4835f12884704f6aa33ac7d14bc" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures", + "hyper 1.7.0", + "jsonwebtoken", + "once_cell", + "prost", + "prost-types", + "reqwest", + "secret-vault-value", + "serde", + "serde_json", + "tokio", + "tonic", + "tonic-prost", + "tower", + "tower-layer", + "tracing", + "url", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1283,8 +1414,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1296,7 +1429,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.4+wasi-0.2.4", + "wasi 0.14.5+wasi-0.2.4", ] [[package]] @@ -1328,10 +1461,12 @@ dependencies = [ "clap", "clap_complete", "confy", + "crc32c", "crossterm", "dialoguer", "dirs", "futures", + "gcloud-sdk", "human-panic", "indoc", "log", @@ -1364,7 +1499,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.0", + "indexmap 2.11.1", "slab", "tokio", "tokio-util", @@ -1383,7 +1518,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.11.0", + "indexmap 2.11.1", "slab", "tokio", "tokio-util", @@ -1509,9 +1644,9 @@ dependencies = [ [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" @@ -1551,6 +1686,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1592,6 +1728,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.7.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.16" @@ -1618,9 +1767,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1766,9 +1915,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -1807,6 +1956,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1822,6 +1981,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1848,6 +2016,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1882,9 +2065,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -1952,12 +2135,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2007,6 +2212,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2131,12 +2346,42 @@ dependencies = [ "subtle", ] +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2156,7 +2401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap 2.11.0", + "indexmap 2.11.1", "quick-xml", "serde", "time", @@ -2298,6 +2543,38 @@ dependencies = [ "unarray", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2450,6 +2727,48 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "async-compression", + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.2", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2508,15 +2827,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -2538,6 +2857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", + "log", "once_cell", "rustls-pki-types", "rustls-webpki 0.103.5", @@ -2691,6 +3011,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secret-vault-value" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471de2a3d4b361569b862e04491696237381641ae5808f8e69ea08de991cd306" +dependencies = [ + "serde", + "serde_json", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2784,6 +3115,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.14.0" @@ -2794,7 +3137,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.0", + "indexmap 2.11.1", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -2822,7 +3165,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.11.1", "itoa", "ryu", "serde", @@ -2882,6 +3225,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.11" @@ -2943,6 +3298,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3104,6 +3468,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -3144,12 +3519,81 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs 0.8.1", + "socket2 0.6.0", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.2", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ + "futures-core", + "futures-util", + "indexmap 2.11.1", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", "tower-layer", "tower-service", ] @@ -3225,10 +3669,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] -name = "unicode-ident" -version = "1.0.18" +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -3388,9 +3838,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.4+wasi-0.2.4" +version = "0.14.5+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.0+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" dependencies = [ "wit-bindgen", ] @@ -3422,6 +3881,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.101" @@ -3454,6 +3926,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3478,13 +3973,13 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link 0.2.0", "windows-result", "windows-strings", ] @@ -3525,20 +4020,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.0", ] [[package]] @@ -3800,6 +4295,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 56b26d7..19b2388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,8 @@ tokio = { version = "1.47.1", features = ["full"] } aws-config = { version = "1.8.6", features = ["behavior-version-latest"] } async-trait = "0.1.89" futures = "0.3.31" +gcloud-sdk = { version = "0.28.1", features = ["google-cloud-secretmanager-v1"] } +crc32c = "0.6.8" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/src/bin/gman/cli.rs b/src/bin/gman/cli.rs index f82c52c..8b3f71f 100644 --- a/src/bin/gman/cli.rs +++ b/src/bin/gman/cli.rs @@ -163,7 +163,7 @@ fn generate_files_secret_injections( secrets: HashMap<&str, String>, run_config: &RunConfig, ) -> Result> { - let re = Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}")?; + let re = Regex::new(r"\{\{(.+)\}\}")?; let mut results = Vec::new(); for file in run_config .files @@ -283,14 +283,14 @@ mod tests { #[test] fn test_generate_files_secret_injections() { let mut secrets = HashMap::new(); - secrets.insert("SECRET1", "value1".to_string()); + secrets.insert("testing/SOME-secret", "value1".to_string()); let temp_dir = tempfile::tempdir().unwrap(); let file_path = temp_dir.path().join("test.txt"); - fs::write(&file_path, "{{SECRET1}}").unwrap(); + fs::write(&file_path, "{{testing/SOME-secret}}").unwrap(); let run_config = RunConfig { name: Some("test".to_string()), - secrets: Some(vec!["SECRET1".to_string()]), + secrets: Some(vec!["testing/SOME-secret".to_string()]), files: Some(vec![file_path.clone()]), flag: None, flag_position: None, @@ -301,7 +301,7 @@ mod tests { assert_eq!(result.len(), 1); assert_eq!(result[0].0, file_path); - assert_str_eq!(result[0].1, "{{SECRET1}}"); + assert_str_eq!(result[0].1, "{{testing/SOME-secret}}"); assert_str_eq!(result[0].2, "value1"); } diff --git a/src/config.rs b/src/config.rs index 0cb5d45..afcdacf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -150,6 +150,10 @@ impl ProviderConfig { debug!("Using AWS Secrets Manager provider"); provider_def } + SupportedProvider::GcpSecretManager { provider_def } => { + debug!("Using GCP Secret Manager provider"); + provider_def + } } } } diff --git a/src/providers/gcp_secret_manager.rs b/src/providers/gcp_secret_manager.rs new file mode 100644 index 0000000..df3cec6 --- /dev/null +++ b/src/providers/gcp_secret_manager.rs @@ -0,0 +1,201 @@ +use crate::providers::SecretProvider; +use anyhow::{Context, Result, anyhow}; +use gcloud_sdk::google::cloud::secretmanager::v1; +use gcloud_sdk::google::cloud::secretmanager::v1::replication::Automatic; +use gcloud_sdk::google::cloud::secretmanager::v1::secret_manager_service_client::SecretManagerServiceClient; +use gcloud_sdk::google::cloud::secretmanager::v1::{ + AccessSecretVersionRequest, AddSecretVersionRequest, CreateSecretRequest, ListSecretsRequest, + Replication, Secret, replication, +}; +use gcloud_sdk::proto_ext::secretmanager::SecretPayload; +use gcloud_sdk::tonic::Code; +use gcloud_sdk::{GoogleApi, GoogleAuthMiddleware}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use v1::DeleteSecretRequest; +use validator::Validate; + +type SecretsManagerClient = GoogleApi>; + +#[skip_serializing_none] +/// Configuration for GCP Secret Manager provider +/// See [GCP Secret Manager](https://cloud.google.com/secret-manager) +/// for more information. +/// +/// This provider stores secrets in GCP Secret Manager. It requires +/// a GCP project ID to be specified. +/// +/// Example +/// ```no_run +/// use gman::providers::{SecretProvider, SupportedProvider}; +/// use gman::config::{Config, ProviderConfig}; +/// use gman::providers::gcp_secret_manager::GcpSecretManagerProvider; +/// +/// let provider = GcpSecretManagerProvider { +/// gcp_project_id: Some("my-gcp-project".to_string()), +/// }; +/// let _ = provider.set_secret("MY_SECRET", "value"); +#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct GcpSecretManagerProvider { + #[validate(required)] + pub gcp_project_id: Option, +} + +#[async_trait::async_trait] +impl SecretProvider for GcpSecretManagerProvider { + fn name(&self) -> &'static str { + "GcpSecretManagerProvider" + } + + async fn get_secret(&self, key: &str) -> Result { + let secret_value = self + .get_client() + .await? + .get() + .access_secret_version(AccessSecretVersionRequest { + name: format!( + "projects/{}/secrets/{}/versions/latest", + self.gcp_project_id.as_ref().unwrap(), + key + ), + }) + .await? + .into_inner() + .payload + .ok_or_else(|| anyhow!("Secret '{}' not found", key))? + .data + .ref_sensitive_value() + .to_vec(); + let secret_string = String::from_utf8(secret_value) + .with_context(|| format!("Invalid UTF-8 in secret '{})'", key))?; + + Ok(secret_string) + } + + async fn set_secret(&self, key: &str, value: &str) -> Result<()> { + let parent = format!("projects/{}", self.gcp_project_id.as_ref().unwrap()); + let secret_name = format!("{}/secrets/{}", parent, key); + let secret = Secret { + replication: Some(Replication { + replication: Some(replication::Replication::Automatic(Automatic { + customer_managed_encryption: None, + })), + }), + ..Default::default() + }; + let client = self.get_client().await?; + + client + .get() + .create_secret(CreateSecretRequest { + parent: parent.clone(), + secret_id: key.to_string(), + secret: Some(secret), + }) + .await + .map_err(|e| { + if e.code() == Code::AlreadyExists { + anyhow!("Secret already exists") + } else { + e.into() + } + })?; + + let bytes = value.as_ref(); + let crc32c = crc32c::crc32c(bytes) as i64; + client + .get() + .add_secret_version(AddSecretVersionRequest { + parent: secret_name, + payload: Some(SecretPayload { + data: bytes.to_vec().into(), + data_crc32c: Some(crc32c), + }), + }) + .await?; + + Ok(()) + } + + async fn delete_secret(&self, key: &str) -> Result<()> { + let name = format!( + "projects/{}/secrets/{}", + self.gcp_project_id.as_ref().unwrap(), + key + ); + self.get_client() + .await? + .get() + .delete_secret(DeleteSecretRequest { + name, + etag: "".to_string(), + }) + .await?; + Ok(()) + } + + async fn update_secret(&self, key: &str, value: &str) -> Result<()> { + let parent = format!( + "projects/{}/secrets/{}", + self.gcp_project_id.as_ref().unwrap(), + key + ); + let bytes = value.as_ref(); + let crc32c = crc32c::crc32c(bytes) as i64; + + self.get_client() + .await? + .get() + .add_secret_version(AddSecretVersionRequest { + parent, + payload: Some(SecretPayload { + data: bytes.to_vec().into(), + data_crc32c: Some(crc32c), + }), + }) + .await?; + + Ok(()) + } + + async fn list_secrets(&self) -> Result> { + let request = ListSecretsRequest { + parent: format!("projects/{}", self.gcp_project_id.as_ref().unwrap()), + ..Default::default() + }; + let secrets = self + .get_client() + .await? + .get() + .list_secrets(request) + .await? + .into_inner() + .secrets + .iter() + .map(|s| { + let full_secret_name = &s.name; + + if let Some(secret_name) = full_secret_name.split("/secrets/").nth(1) { + secret_name.to_string() + } else { + full_secret_name.to_string() + } + }) + .collect(); + Ok(secrets) + } +} + +impl GcpSecretManagerProvider { + async fn get_client(&self) -> Result { + let client = GoogleApi::from_function( + SecretManagerServiceClient::new, + "https://secretmanager.googleapis.com", + None, + ) + .await?; + + Ok(client) + } +} diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 3aeb563..385dabc 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -3,6 +3,7 @@ //! Implementations provide storage/backends for secrets and a common //! interface used by the CLI. pub mod aws_secrets_manager; +pub mod gcp_secret_manager; mod git_sync; pub mod local; @@ -11,6 +12,8 @@ use anyhow::{Result, anyhow}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use validator::{Validate, ValidationErrors}; +use aws_secrets_manager::AwsSecretsManagerProvider; +use gcp_secret_manager::GcpSecretManagerProvider; /// A secret storage backend capable of CRUD, with optional /// update, listing, and sync support. @@ -43,7 +46,6 @@ pub trait SecretProvider: Send + Sync { /// Registry of built-in providers. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")] -//TODO test that this works with the AWS config pub enum SupportedProvider { Local { #[serde(flatten)] @@ -51,7 +53,11 @@ pub enum SupportedProvider { }, AwsSecretsManager { #[serde(flatten)] - provider_def: aws_secrets_manager::AwsSecretsManagerProvider, + provider_def: AwsSecretsManagerProvider, + }, + GcpSecretManager { + #[serde(flatten)] + provider_def: GcpSecretManagerProvider, }, } @@ -60,6 +66,7 @@ impl Validate for SupportedProvider { match self { SupportedProvider::Local { provider_def } => provider_def.validate(), SupportedProvider::AwsSecretsManager { provider_def } => provider_def.validate(), + SupportedProvider::GcpSecretManager { provider_def } => provider_def.validate(), } } } @@ -77,6 +84,7 @@ impl Display for SupportedProvider { match self { SupportedProvider::Local { .. } => write!(f, "local"), SupportedProvider::AwsSecretsManager { .. } => write!(f, "aws_secrets_manager"), + SupportedProvider::GcpSecretManager { .. } => write!(f, "gcp_secret_manager"), } } }