From fb591a55b69b5a2a26229631d691f674c9f88f29 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 18 Mar 2025 20:31:42 +0100 Subject: [PATCH] feat: Encode remaining supported tokens --- src/api/v3/types.rs | 2 +- src/token/application_credential.rs | 80 ++++++++++++++++++++++++- src/token/domain_scoped.rs | 78 ++++++++++++++++++++++++- src/token/fernet.rs | 90 +++++++++++++++++++++++++++-- src/token/project_scoped.rs | 77 +++++++++++++++++++++++- src/token/unscoped.rs | 3 +- 6 files changed, 314 insertions(+), 16 deletions(-) diff --git a/src/api/v3/types.rs b/src/api/v3/types.rs index 5c664b35..a7683589 100644 --- a/src/api/v3/types.rs +++ b/src/api/v3/types.rs @@ -35,7 +35,7 @@ impl TryFrom<&BackendToken> for Token { token.user_id(data.user_id.clone()); token.methods(data.methods.clone()); token.audit_ids(data.audit_ids.clone()); - token.expires_at(data.expires_at.clone()); + token.expires_at(data.expires_at); } Ok(token.build()?) } diff --git a/src/token/application_credential.rs b/src/token/application_credential.rs index 7cfebefa..969d8a6b 100644 --- a/src/token/application_credential.rs +++ b/src/token/application_credential.rs @@ -13,9 +13,10 @@ // SPDX-License-Identifier: Apache-2.0 use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use rmp::{decode::read_pfix, encode::write_pfix}; use std::collections::BTreeMap; - -use rmp::decode::*; +use std::io::Write; use crate::token::{ error::TokenProviderError, @@ -24,16 +25,42 @@ use crate::token::{ types::Token, }; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq)] pub struct ApplicationCredentialToken { pub user_id: String, + #[builder(default, setter(name = _methods))] pub methods: Vec, + #[builder(default, setter(name = _audit_ids))] pub audit_ids: Vec, pub expires_at: DateTime, pub project_id: String, pub application_credential_id: String, } +impl ApplicationCredentialTokenBuilder { + pub fn methods(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.methods + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } + + pub fn audit_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.audit_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } +} + impl From for Token { fn from(value: ApplicationCredentialToken) -> Self { Token::ApplicationCredential(value) @@ -43,6 +70,25 @@ impl From for Token { impl MsgPackToken for ApplicationCredentialToken { type Token = ApplicationCredentialToken; + fn assemble( + &self, + wd: &mut W, + auth_map: &BTreeMap, + ) -> Result<(), TokenProviderError> { + fernet_utils::write_uuid(wd, &self.user_id)?; + write_pfix( + wd, + fernet::encode_auth_methods(self.methods.clone(), auth_map)? as u8, + ) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + fernet_utils::write_uuid(wd, &self.project_id)?; + fernet_utils::write_time(wd, self.expires_at)?; + fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + fernet_utils::write_uuid(wd, &self.application_credential_id)?; + + Ok(()) + } + fn disassemble( rd: &mut &[u8], auth_map: &BTreeMap, @@ -67,3 +113,31 @@ impl MsgPackToken for ApplicationCredentialToken { }) } } + +#[cfg(test)] +mod tests { + use chrono::{Local, SubsecRound}; + use uuid::Uuid; + + use super::*; + + #[test] + fn test_roundtrip() { + let token = ApplicationCredentialToken { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + project_id: Uuid::new_v4().simple().to_string(), + application_credential_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + }; + let auth_map = BTreeMap::from([(1, "password".into())]); + let mut buf = vec![]; + token.assemble(&mut buf, &auth_map).unwrap(); + let encoded_buf = buf.clone(); + let decoded = + ApplicationCredentialToken::disassemble(&mut encoded_buf.as_slice(), &auth_map) + .unwrap(); + assert_eq!(token, decoded); + } +} diff --git a/src/token/domain_scoped.rs b/src/token/domain_scoped.rs index 9ef17380..c50c30a1 100644 --- a/src/token/domain_scoped.rs +++ b/src/token/domain_scoped.rs @@ -13,9 +13,10 @@ // SPDX-License-Identifier: Apache-2.0 use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use rmp::{decode::read_pfix, encode::write_pfix}; use std::collections::BTreeMap; - -use rmp::decode::*; +use std::io::Write; use crate::token::{ error::TokenProviderError, @@ -24,15 +25,41 @@ use crate::token::{ types::Token, }; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq)] pub struct DomainScopeToken { pub user_id: String, + #[builder(default, setter(name = _methods))] pub methods: Vec, + #[builder(default, setter(name = _audit_ids))] pub audit_ids: Vec, pub expires_at: DateTime, pub domain_id: String, } +impl DomainScopeTokenBuilder { + pub fn methods(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.methods + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } + + pub fn audit_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.audit_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } +} + impl From for Token { fn from(value: DomainScopeToken) -> Self { Token::DomainScope(value) @@ -41,6 +68,25 @@ impl From for Token { impl MsgPackToken for DomainScopeToken { type Token = DomainScopeToken; + + fn assemble( + &self, + wd: &mut W, + auth_map: &BTreeMap, + ) -> Result<(), TokenProviderError> { + fernet_utils::write_uuid(wd, &self.user_id)?; + write_pfix( + wd, + fernet::encode_auth_methods(self.methods.clone(), auth_map)? as u8, + ) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + fernet_utils::write_uuid(wd, &self.domain_id)?; + fernet_utils::write_time(wd, self.expires_at)?; + fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + + Ok(()) + } + fn disassemble( rd: &mut &[u8], auth_map: &BTreeMap, @@ -62,3 +108,29 @@ impl MsgPackToken for DomainScopeToken { }) } } + +#[cfg(test)] +mod tests { + use chrono::{Local, SubsecRound}; + use uuid::Uuid; + + use super::*; + + #[test] + fn test_roundtrip() { + let token = DomainScopeToken { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + domain_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + }; + let auth_map = BTreeMap::from([(1, "password".into())]); + let mut buf = vec![]; + token.assemble(&mut buf, &auth_map).unwrap(); + let encoded_buf = buf.clone(); + let decoded = + DomainScopeToken::disassemble(&mut encoded_buf.as_slice(), &auth_map).unwrap(); + assert_eq!(token, decoded); + } +} diff --git a/src/token/fernet.rs b/src/token/fernet.rs index 3c54afa0..307cb76f 100644 --- a/src/token/fernet.rs +++ b/src/token/fernet.rs @@ -113,7 +113,7 @@ pub(crate) fn encode_auth_methods>( impl FernetTokenProvider { /// Parse binary blob as MessagePack after encrypting it with Fernet - fn parse(&self, rd: &mut &[u8]) -> Result { + fn decode(&self, rd: &mut &[u8]) -> Result { if let Marker::FixArray(_) = read_marker(rd).map_err(ValueReadError::from)? { match read_payload_token_type(rd)? { 0 => Ok(UnscopedToken::disassemble(rd, &self.auth_map)?.into()), @@ -138,8 +138,26 @@ impl FernetTokenProvider { .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; data.assemble(&mut buf, &self.auth_map)?; } - _ => { - todo!() + Token::DomainScope(data) => { + write_array_len(&mut buf, 6) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + write_pfix(&mut buf, 1) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + data.assemble(&mut buf, &self.auth_map)?; + } + Token::ProjectScope(data) => { + write_array_len(&mut buf, 6) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + write_pfix(&mut buf, 2) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + data.assemble(&mut buf, &self.auth_map)?; + } + Token::ApplicationCredential(data) => { + write_array_len(&mut buf, 7) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + write_pfix(&mut buf, 9) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + data.assemble(&mut buf, &self.auth_map)?; } } Ok(buf.into()) @@ -173,7 +191,7 @@ impl FernetTokenProvider { Some(fernet) => fernet.decrypt(credential)?, _ => self.get_fernet()?.decrypt(credential)?, }; - self.parse(&mut payload.as_slice()) + self.decode(&mut payload.as_slice()) } /// Encrypt the token @@ -224,6 +242,7 @@ pub(super) mod tests { use std::io::Write; use std::path::PathBuf; use tempfile::tempdir; + use uuid::Uuid; fn setup_config() -> Config { let keys_dir = tempdir().unwrap(); @@ -270,7 +289,7 @@ pub(super) mod tests { #[tokio::test] async fn test_unscoped_roundtrip() { let token = Token::Unscoped(UnscopedToken { - user_id: "abc".into(), + user_id: Uuid::new_v4().simple().to_string(), methods: vec!["password".into()], audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(), @@ -309,6 +328,26 @@ pub(super) mod tests { } } + #[tokio::test] + async fn test_domain_roundtrip() { + let token = Token::DomainScope(DomainScopeToken { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + domain_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + }); + + let mut backend = FernetTokenProvider::default(); + let config = crate::tests::token::setup_config(); + backend.set_config(config); + backend.load_keys().unwrap(); + + let encrypted = backend.encrypt(&token).unwrap(); + let dec_token = backend.decrypt(&encrypted).unwrap(); + assert_eq!(token, dec_token); + } + #[tokio::test] async fn test_decrypt_project() { let token = "gAAAAABns2ixy75K_KfoosWLrNNqG6KW8nm3Xzv0_2dOx8ODWH7B8i2g8CncGLO6XBEH_TYLg83P6XoKQ5bU8An8Kqgw9WX3bvmEQXphnwPM6aRAOQUSdVhTlUm_8otDG9BS2rc70Q7pfy57S3_yBgimy-174aKdP8LPusvdHZsQPEJO9pfeXWw"; @@ -332,6 +371,26 @@ pub(super) mod tests { } } + #[tokio::test] + async fn test_project_roundtrip() { + let token = Token::ProjectScope(ProjectScopeToken { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + project_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + }); + + let mut backend = FernetTokenProvider::default(); + let config = crate::tests::token::setup_config(); + backend.set_config(config); + backend.load_keys().unwrap(); + + let encrypted = backend.encrypt(&token).unwrap(); + let dec_token = backend.decrypt(&encrypted).unwrap(); + assert_eq!(token, dec_token); + } + #[tokio::test] async fn test_decrypt_application_credential() { let token = "gAAAAABnt11m57ZlI9JU0g2BKJw2EN-InbAIijcIG7SxvPATntgTlcTMwha-Fh7isNNIwDq2WaWglV1nYgftfoUK245ZnEJ0_gXaIhl6COhNommYv2Bs9PnJqfgrrxrIrB8rh4pfeyCtMkv5ePYgFFPyRFE37l3k7qL5p7qVhYT37yT1-K5lYAV0f6Vy70h3KX1HO0m6Rl90"; @@ -358,4 +417,25 @@ pub(super) mod tests { panic!() } } + + #[tokio::test] + async fn test_application_credential_roundtrip() { + let token = Token::ApplicationCredential(ApplicationCredentialToken { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["application_credential".into()], + project_id: Uuid::new_v4().simple().to_string(), + application_credential_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + }); + + let mut backend = FernetTokenProvider::default(); + let config = crate::tests::token::setup_config(); + backend.set_config(config); + backend.load_keys().unwrap(); + + let encrypted = backend.encrypt(&token).unwrap(); + let dec_token = backend.decrypt(&encrypted).unwrap(); + assert_eq!(token, dec_token); + } } diff --git a/src/token/project_scoped.rs b/src/token/project_scoped.rs index 51526695..99bb53bc 100644 --- a/src/token/project_scoped.rs +++ b/src/token/project_scoped.rs @@ -13,9 +13,10 @@ // SPDX-License-Identifier: Apache-2.0 use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use rmp::{decode::read_pfix, encode::write_pfix}; use std::collections::BTreeMap; - -use rmp::decode::*; +use std::io::Write; use crate::token::{ error::TokenProviderError, @@ -24,15 +25,41 @@ use crate::token::{ types::Token, }; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Builder, Clone, Debug, Default, PartialEq)] pub struct ProjectScopeToken { pub user_id: String, + #[builder(default, setter(name = _methods))] pub methods: Vec, + #[builder(default, setter(name = _audit_ids))] pub audit_ids: Vec, pub expires_at: DateTime, pub project_id: String, } +impl ProjectScopeTokenBuilder { + pub fn methods(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.methods + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } + + pub fn audit_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.audit_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } +} + impl From for Token { fn from(value: ProjectScopeToken) -> Self { Token::ProjectScope(value) @@ -42,6 +69,24 @@ impl From for Token { impl MsgPackToken for ProjectScopeToken { type Token = ProjectScopeToken; + fn assemble( + &self, + wd: &mut W, + auth_map: &BTreeMap, + ) -> Result<(), TokenProviderError> { + fernet_utils::write_uuid(wd, &self.user_id)?; + write_pfix( + wd, + fernet::encode_auth_methods(self.methods.clone(), auth_map)? as u8, + ) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + fernet_utils::write_uuid(wd, &self.project_id)?; + fernet_utils::write_time(wd, self.expires_at)?; + fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + + Ok(()) + } + fn disassemble( rd: &mut &[u8], auth_map: &BTreeMap, @@ -63,3 +108,29 @@ impl MsgPackToken for ProjectScopeToken { }) } } + +#[cfg(test)] +mod tests { + use chrono::{Local, SubsecRound}; + use uuid::Uuid; + + use super::*; + + #[test] + fn test_roundtrip() { + let token = ProjectScopeToken { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + project_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + }; + let auth_map = BTreeMap::from([(1, "password".into())]); + let mut buf = vec![]; + token.assemble(&mut buf, &auth_map).unwrap(); + let encoded_buf = buf.clone(); + let decoded = + ProjectScopeToken::disassemble(&mut encoded_buf.as_slice(), &auth_map).unwrap(); + assert_eq!(token, decoded); + } +} diff --git a/src/token/unscoped.rs b/src/token/unscoped.rs index 0370dc69..aad4125b 100644 --- a/src/token/unscoped.rs +++ b/src/token/unscoped.rs @@ -109,13 +109,14 @@ impl MsgPackToken for UnscopedToken { #[cfg(test)] mod tests { use chrono::{Local, SubsecRound}; + use uuid::Uuid; use super::*; #[test] fn test_roundtrip() { let token = UnscopedToken { - user_id: "abc".into(), + user_id: Uuid::new_v4().simple().to_string(), methods: vec!["password".into()], audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(),