diff --git a/Cargo.lock b/Cargo.lock index 461b70c..79fe66a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,7 @@ dependencies = [ "config", "futures", "futures-core", + "jsonwebtoken", "log", "lupus", "opentelemetry", @@ -192,6 +193,12 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -438,6 +445,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -461,6 +480,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -508,7 +528,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -543,12 +565,27 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ + "pkcs8", "signature", ] @@ -560,9 +597,11 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "serde", "sha2", "signature", "subtle", + "zeroize", ] [[package]] @@ -571,6 +610,27 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email_address" version = "0.2.9" @@ -633,6 +693,16 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -801,6 +871,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -842,6 +913,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.13" @@ -902,6 +984,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[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 = "1.4.0" @@ -1282,11 +1382,38 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "jsonwebtoken" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881733cbc631fc9e472e24447ce32a64bedf2da498d6d8570b08edc87de71f65" +dependencies = [ + "base64", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "pem", + "rand 0.8.5", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", + "simple_asn1", + "zeroize", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -1300,6 +1427,12 @@ version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1445,6 +1578,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-cmp" version = "0.1.0" @@ -1503,6 +1652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1622,6 +1772,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1688,6 +1862,16 @@ dependencies = [ "serde", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1782,6 +1966,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -1838,6 +2033,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2131,6 +2335,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2159,6 +2373,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -2296,6 +2530,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2483,6 +2731,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -2505,6 +2765,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.7.3" @@ -3600,6 +3866,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 5020376..c1bc9e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ uuid = { version = "1.18.0", features = ["v4"] } serde = "1.0.228" futures-core = "0.3.32" config = "0.15.25" +jsonwebtoken = { version = "11.0.0", features = ["rust_crypto"] } diff --git a/src/authorization/mod.rs b/src/authorization/mod.rs index de39045..2e4ad4b 100644 --- a/src/authorization/mod.rs +++ b/src/authorization/mod.rs @@ -1,15 +1,30 @@ //! Bearer-token helpers shared by every gRPC client and server in Aquila: //! [`authorization::get_authentication_metadata`] to attach a token to an //! outgoing request, [`authorization::extract_token`] to read one back off -//! an incoming request. +//! an incoming request, and [`authorization::verify_jwt`] to check a +//! presented token against the pre-provisioned secret for an action or +//! runtime identity. pub mod authorization { use std::str::FromStr; + use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; + use serde::Deserialize; use tonic::{ Request, Status, metadata::{MetadataMap, MetadataValue}, }; + /// Claims carried by an action/runtime authentication JWT. `sub` must + /// match the identity (action identifier or runtime family) the token is + /// presented for, so a secret leaked for one identity can't be replayed + /// to authenticate as another. + #[derive(Debug, Deserialize)] + struct Claims { + sub: String, + #[allow(dead_code)] + exp: u64, + } + /// get_authentication_metadata /// /// Creates a `MetadataMap` that contains the defined token as a value of the `authentication` key. @@ -58,4 +73,42 @@ pub mod authorization { Ok(token) } + + /// Verifies `token` is a JWT signed with `secret` (HS256), not expired, + /// and issued for `expected_subject` - the pre-provisioned identity + /// (action identifier or runtime family) `secret` was configured for. + /// + /// # Examples + /// + /// ``` + /// use aquila_grpc::verify_jwt; + /// use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; + /// use serde::Serialize; + /// + /// #[derive(Serialize)] + /// struct Claims { sub: String, exp: u64 } + /// + /// let token = encode( + /// &Header::new(Algorithm::HS256), + /// &Claims { sub: "taurus".to_string(), exp: 4_102_444_800 }, + /// &EncodingKey::from_secret(b"secret"), + /// ).unwrap(); + /// + /// assert!(verify_jwt(&token, "secret", "taurus")); + /// assert!(!verify_jwt(&token, "secret", "draco-rest")); + /// assert!(!verify_jwt(&token, "wrong-secret", "taurus")); + /// ``` + pub fn verify_jwt(token: &str, secret: &str, expected_subject: &str) -> bool { + let mut validation = Validation::new(Algorithm::HS256); + validation.set_required_spec_claims(&["sub", "exp"]); + let decoding_key = DecodingKey::from_secret(secret.as_bytes()); + + match decode::(token, &decoding_key, &validation) { + Ok(data) => data.claims.sub == expected_subject, + Err(err) => { + log::debug!("JWT verification failed: {:?}", err); + false + } + } + } } diff --git a/src/configuration/service/mod.rs b/src/configuration/service/mod.rs index 059dacd..14b20b4 100644 --- a/src/configuration/service/mod.rs +++ b/src/configuration/service/mod.rs @@ -15,6 +15,7 @@ use std::{fs::File, io::Read, path::Path}; use serde_json::from_str; use tucana::shared::ModuleConfigurations; +use crate::authorization::authorization::verify_jwt; use dto::SerializableServiceConfiguration; #[derive(Clone)] @@ -55,32 +56,42 @@ impl ServiceConfiguration { self.has_runtime(token, name) || self.has_action(token, name) } - pub fn has_runtime(&self, token: &String, runtime_name: &String) -> bool { - let name = match Self::extract_service_name(runtime_name) { - Some(n) => n, - None => return false, - }; - + /// Resolves the runtime config entry `runtime_name` authenticates as, + /// keyed by its family identifier (e.g. every `taurus-*` instance + /// resolves to the one `taurus` entry) - and verified against `token`. + /// Multiple entries may share an identifier (e.g. during a secret + /// rotation window), so each is tried until one verifies. + fn find_runtime(&self, token: &String, runtime_name: &String) -> Option<&RuntimeServiceConfiguration> { + let name = Self::extract_service_name(runtime_name)?; self.runtimes .iter() - .find(|x| &x.token == token && x.identifier == name) - .is_some() + .filter(|x| x.identifier == name) + .find(|x| verify_jwt(token, &x.token, &x.identifier)) } - pub fn has_action(&self, token: &String, action_name: &String) -> bool { + /// Resolves the action config entry registered under `action_name` and + /// verified against `token` - see [`Self::find_runtime`] on why more + /// than one entry may need to be tried. + fn find_action(&self, token: &String, action_name: &String) -> Option<&ActionServiceConfiguration> { self.actions .iter() - .find(|x| &x.token == token && &x.service_name == action_name) - .is_some() + .filter(|x| &x.service_name == action_name) + .find(|x| verify_jwt(token, &x.token, &x.service_name)) + } + + pub fn has_runtime(&self, token: &String, runtime_name: &String) -> bool { + self.find_runtime(token, runtime_name).is_some() + } + + pub fn has_action(&self, token: &String, action_name: &String) -> bool { + self.find_action(token, action_name).is_some() } /// The configured replica count for a `token`/`action_identifier` pair, /// or `1` if the action isn't registered - a missing action never gets /// this far anyway, since [`Self::has_action`] gates logon first. pub fn action_replicas(&self, token: &String, action_identifier: &String) -> u32 { - self.actions - .iter() - .find(|x| &x.token == token && &x.service_name == action_identifier) + self.find_action(token, action_identifier) .map(|a| a.replicas) .unwrap_or(1) } @@ -90,11 +101,7 @@ impl ServiceConfiguration { token: &String, action_identifier: &String, ) -> Vec { - match self - .actions - .iter() - .find(|x| &x.token == token && &x.service_name == action_identifier) - { + match self.find_action(token, action_identifier) { Some(a) => a.config.clone(), None => vec![], } @@ -178,18 +185,41 @@ mod tests { SerializableModuleProjectConfiguration, SerializableServiceConfiguration, }, }; + use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; + use serde::Serialize; + + #[derive(Serialize)] + struct Claims { + sub: String, + exp: u64, + } + + /// Mints a JWT the way an action/runtime client would, signed with + /// `secret` (the value configured as that identity's `token`) and + /// carrying `subject` (its identifier) as `sub`. + fn jwt(secret: &str, subject: &str) -> String { + encode( + &Header::new(Algorithm::HS256), + &Claims { + sub: subject.to_string(), + exp: 4_102_444_800, // 2100-01-01, far enough out to never expire in tests + }, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .unwrap() + } fn fixture() -> ServiceConfiguration { SerializableServiceConfiguration { actions: vec![SerializableActionServiceConfiguration { - token: String::from("action-token"), + token: String::from("action-secret"), identifier: String::from("action-identifier"), configs: vec![], replicas: 1, }], runtimes: vec![ RuntimeServiceConfiguration { - token: String::from("taurus-token"), + token: String::from("taurus-secret"), identifier: String::from("taurus"), resolved_modules: vec![ String::from("taurus-boolean"), @@ -197,12 +227,12 @@ mod tests { ], }, RuntimeServiceConfiguration { - token: String::from("draco-rest-token"), + token: String::from("draco-rest-secret"), identifier: String::from("draco-rest"), resolved_modules: vec![], }, RuntimeServiceConfiguration { - token: String::from("draco-cron-token"), + token: String::from("draco-cron-secret"), identifier: String::from("draco-cron"), resolved_modules: vec![], }, @@ -216,47 +246,74 @@ mod tests { let config = fixture(); assert!(config.has_runtime( - &String::from("taurus-token"), + &jwt("taurus-secret", "taurus"), &String::from("taurus-runtime-01") )); - assert!(config.has_runtime(&String::from("taurus-token"), &String::from("taurus"))); + assert!(config.has_runtime(&jwt("taurus-secret", "taurus"), &String::from("taurus"))); assert!(config.has_runtime( - &String::from("draco-rest-token"), + &jwt("draco-rest-secret", "draco-rest"), &String::from("draco-rest") )); assert!(config.has_runtime( - &String::from("draco-cron-token"), + &jwt("draco-cron-secret", "draco-cron"), &String::from("draco-cron") )); - assert!(!config.has_runtime(&String::from("taurus-token"), &String::from("draco-rest"))); - assert!(!config.has_runtime(&String::from("draco-rest-token"), &String::from("taurus-x"))); assert!(!config.has_runtime( - &String::from("taurus-token"), + &jwt("taurus-secret", "taurus"), + &String::from("draco-rest") + )); + assert!(!config.has_runtime( + &jwt("draco-rest-secret", "draco-rest"), + &String::from("taurus-x") + )); + assert!(!config.has_runtime( + &jwt("taurus-secret", "taurus"), &String::from("unknown-runtime") )); } #[test] - fn has_action_requires_exact_identifier_and_matching_token() { + fn has_runtime_rejects_wrong_secret_wrong_subject_or_plain_token() { + let config = fixture(); + + assert!(!config.has_runtime( + &jwt("wrong-secret", "taurus"), + &String::from("taurus-runtime-01") + )); + assert!(!config.has_runtime( + &jwt("taurus-secret", "draco-rest"), + &String::from("taurus-runtime-01") + )); + assert!(!config.has_runtime( + &String::from("taurus-secret"), + &String::from("taurus-runtime-01") + )); + } + + #[test] + fn has_action_requires_matching_identifier_and_jwt() { let config = fixture(); assert!(config.has_action( - &String::from("action-token"), + &jwt("action-secret", "action-identifier"), &String::from("action-identifier") )); assert!(!config.has_action( - &String::from("taurus-token"), + &jwt("taurus-secret", "taurus"), &String::from("action-identifier") )); - assert!(!config.has_action(&String::from("action-token"), &String::from("action-other"))); - assert!(!config.has_action(&String::from("example"), &String::from("example"))); + assert!(!config.has_action( + &jwt("action-secret", "action-identifier"), + &String::from("action-other") + )); + assert!(!config.has_action(&String::from("action-secret"), &String::from("example"))); } #[test] fn action_replicas_reads_the_configured_count_and_defaults_to_one() { let config: ServiceConfiguration = SerializableServiceConfiguration { actions: vec![SerializableActionServiceConfiguration { - token: String::from("action-token"), + token: String::from("action-secret"), identifier: String::from("action-identifier"), configs: vec![], replicas: 3, @@ -267,7 +324,7 @@ mod tests { assert_eq!( config.action_replicas( - &String::from("action-token"), + &jwt("action-secret", "action-identifier"), &String::from("action-identifier") ), 3 @@ -282,20 +339,26 @@ mod tests { fn has_service_returns_true_for_valid_runtime_or_action_pairings() { let config = fixture(); - assert!(config.has_service(&String::from("taurus-token"), &String::from("taurus-x"))); assert!(config.has_service( - &String::from("draco-rest-token"), + &jwt("taurus-secret", "taurus"), + &String::from("taurus-x") + )); + assert!(config.has_service( + &jwt("draco-rest-secret", "draco-rest"), &String::from("draco-rest") )); assert!(config.has_service( - &String::from("action-token"), + &jwt("action-secret", "action-identifier"), &String::from("action-identifier") )); assert!(!config.has_service( - &String::from("draco-rest-token"), + &jwt("draco-rest-secret", "draco-rest"), &String::from("action-identifier") )); - assert!(!config.has_service(&String::from("action-token"), &String::from("taurus-x"))); + assert!(!config.has_service( + &jwt("action-secret", "action-identifier"), + &String::from("taurus-x") + )); } #[test] @@ -315,11 +378,11 @@ mod tests { } #[test] - fn get_action_configuration_requires_matching_token_and_identifier() { + fn get_action_configuration_requires_matching_jwt_and_identifier() { let config: ServiceConfiguration = SerializableServiceConfiguration { actions: vec![ SerializableActionServiceConfiguration { - token: String::from("old-token"), + token: String::from("old-secret"), identifier: String::from("shared-action"), configs: vec![SerializableModuleProjectConfiguration { project_id: 1, @@ -331,7 +394,7 @@ mod tests { replicas: 1, }, SerializableActionServiceConfiguration { - token: String::from("new-token"), + token: String::from("new-secret"), identifier: String::from("shared-action"), configs: vec![SerializableModuleProjectConfiguration { project_id: 2, @@ -347,22 +410,40 @@ mod tests { } .into(); - let configs = config - .get_action_configuration(&String::from("new-token"), &String::from("shared-action")); + // Both the old and the new secret authenticate during a rotation + // window - each is a distinct config entry sharing the identifier. + let old_configs = config.get_action_configuration( + &jwt("old-secret", "shared-action"), + &String::from("shared-action"), + ); + assert_eq!(old_configs[0].module_configurations[0].project_id, 1); + + let new_configs = config.get_action_configuration( + &jwt("new-secret", "shared-action"), + &String::from("shared-action"), + ); + assert_eq!(new_configs.len(), 1); + assert_eq!(new_configs[0].module_identifier, "shared-action"); + assert_eq!(new_configs[0].module_configurations[0].project_id, 2); - assert_eq!(configs.len(), 1); - assert_eq!(configs[0].module_identifier, "shared-action"); - assert_eq!(configs[0].module_configurations[0].project_id, 2); + assert!( + config + .get_action_configuration( + &jwt("wrong-secret", "shared-action"), + &String::from("shared-action") + ) + .is_empty() + ); } #[test] - fn get_action_configuration_returns_empty_for_identifier_with_wrong_token() { + fn get_action_configuration_returns_empty_for_identifier_with_wrong_secret() { let config = fixture(); assert!( config .get_action_configuration( - &String::from("wrong-token"), + &jwt("wrong-secret", "action-identifier"), &String::from("action-identifier") ) .is_empty()