From 2c153f91932cf78cf71ac54440efb72c19da1620 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 24 Mar 2025 19:49:53 +0100 Subject: [PATCH] feat: Expand tokenprovider with more helper methods Add methods (together with placeholders inside the token structure) to keep data like roles, project/domain directly in the backend token. --- src/api/error.rs | 3 +- src/api/v3/auth/token/common.rs | 57 ++--- src/api/v3/auth/token/mod.rs | 59 ++++- src/assignment/error.rs | 6 + src/token/application_credential.rs | 9 + src/token/domain_scoped.rs | 9 + src/token/error.rs | 14 ++ src/token/fernet.rs | 3 + src/token/mod.rs | 323 +++++++++++++++++++++++++++- src/token/project_scoped.rs | 9 + typos.toml | 2 +- 11 files changed, 445 insertions(+), 49 deletions(-) diff --git a/src/api/error.rs b/src/api/error.rs index ddfac631..6d16bbbd 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -113,7 +113,8 @@ pub enum KeystoneApiError { impl IntoResponse for KeystoneApiError { fn into_response(self) -> Response { - error!("Error happened during request processing: {:?}", self); + error!("Error happened during request processing: {:#?}", self); + //tracing::debug!("stacktrace: {}", self.backtrace()); match self { KeystoneApiError::Conflict(_) => ( StatusCode::CONFLICT, diff --git a/src/api/v3/auth/token/common.rs b/src/api/v3/auth/token/common.rs index 8f0da495..94a287e3 100644 --- a/src/api/v3/auth/token/common.rs +++ b/src/api/v3/auth/token/common.rs @@ -16,8 +16,6 @@ use crate::api::common; use crate::api::error::{KeystoneApiError, TokenError}; use crate::api::v3::auth::token::types::{ProjectBuilder, Token, TokenBuilder, UserBuilder}; use crate::api::v3::role::types::Role; -use crate::assignment::AssignmentApi; -use crate::assignment::types::RoleAssignmentListParametersBuilder; use crate::identity::{IdentityApi, types::UserResponse}; use crate::keystone::ServiceState; use crate::resource::{ @@ -55,12 +53,12 @@ impl Token { } ProviderToken::DomainScope(_token) => { response.domain(domain.ok_or(KeystoneApiError::InternalError( - "domain scope missing".to_string(), + "domain scope information missing".to_string(), ))?); } ProviderToken::ProjectScope(token) => { let project = project.ok_or(KeystoneApiError::InternalError( - "domain scope missing".to_string(), + "project scope information missing".to_string(), ))?; let mut project_response = ProjectBuilder::default(); @@ -75,26 +73,12 @@ impl Token { } response.project(project_response.build().map_err(TokenError::from)?); - let token_roles = state - .provider - .get_assignment_provider() - .list_role_assignments( - &state.db, - &state.provider, - &RoleAssignmentListParametersBuilder::default() - .user_id(user.id.clone()) - .project_id(&token.project_id) - .build()?, - ) - .await?; response.roles( - token_roles + token + .roles + .clone() .into_iter() - .map(|x| Role { - id: x.role_id.clone(), - name: x.role_name.clone().unwrap_or_default(), - ..Default::default() - }) + .map(Into::into) .collect::>(), ); } @@ -171,26 +155,12 @@ impl Token { } response.project(project_response.build().map_err(TokenError::from)?); - let token_roles = state - .provider - .get_assignment_provider() - .list_role_assignments( - &state.db, - &state.provider, - &RoleAssignmentListParametersBuilder::default() - .user_id(user.id) - .project_id(&token.project_id) - .build()?, - ) - .await?; response.roles( - token_roles + token + .roles + .clone() .into_iter() - .map(|x| Role { - id: x.role_id.clone(), - name: x.role_name.clone().unwrap_or_default(), - ..Default::default() - }) + .map(Into::into) .collect::>(), ); } @@ -211,7 +181,7 @@ mod tests { use crate::api::v3::role::types::Role; use crate::assignment::{ MockAssignmentProvider, - types::{Assignment, AssignmentType, RoleAssignmentListParameters}, + types::{Assignment, AssignmentType, Role as ProviderRole, RoleAssignmentListParameters}, }; use crate::config::Config; use crate::identity::{MockIdentityProvider, types::UserResponse}; @@ -402,6 +372,11 @@ mod tests { &ProviderToken::ProjectScope(ProjectScopeToken { user_id: "bar".into(), project_id: "project_id".into(), + roles: vec![ProviderRole { + id: "rid".into(), + name: "role_name".into(), + ..Default::default() + }], ..Default::default() }), ) diff --git a/src/api/v3/auth/token/mod.rs b/src/api/v3/auth/token/mod.rs index 0f1b82ed..aefaa70a 100644 --- a/src/api/v3/auth/token/mod.rs +++ b/src/api/v3/auth/token/mod.rs @@ -131,7 +131,7 @@ async fn post( } if let Some(authed_user) = &user { - let token = state.provider.get_token_provider().issue_token( + let mut token = state.provider.get_token_provider().issue_token( authed_user.id.clone(), methods, Vec::::from([URL_SAFE @@ -142,6 +142,24 @@ async fn post( domain.as_ref(), )?; + state + .provider + .get_token_provider() + .populate_role_assignments(&mut token, &state.db, &state.provider) + .await?; + + state + .provider + .get_token_provider() + .expand_project_information(&mut token, &state.db, &state.provider) + .await?; + + state + .provider + .get_token_provider() + .expand_domain_information(&mut token, &state.db, &state.provider) + .await?; + let api_token = TokenResponse { token: ApiResponseToken::from_user_auth( &state, @@ -194,13 +212,31 @@ async fn show( .map_err(|_| KeystoneApiError::InvalidHeader)? .to_string(); - let token = state + let mut token = state .provider .get_token_provider() .validate_token(&subject_token, None) .await .map_err(|_| KeystoneApiError::InvalidToken)?; + state + .provider + .get_token_provider() + .populate_role_assignments(&mut token, &state.db, &state.provider) + .await?; + + state + .provider + .get_token_provider() + .expand_project_information(&mut token, &state.db, &state.provider) + .await?; + + state + .provider + .get_token_provider() + .expand_domain_information(&mut token, &state.db, &state.provider) + .await?; + let response_token = ApiResponseToken::from_provider_token(&state, &token).await?; Ok(TokenResponse { @@ -266,6 +302,15 @@ mod tests { ..Default::default() })) }); + token_mock + .expect_populate_role_assignments() + .returning(|_, _, _| Ok(())); + token_mock + .expect_expand_project_information() + .returning(|_, _, _| Ok(())); + token_mock + .expect_expand_domain_information() + .returning(|_, _, _| Ok(())); let provider = ProviderBuilder::default() .config(config.clone()) @@ -392,7 +437,15 @@ mod tests { ..Default::default() })) }); - + token_mock + .expect_populate_role_assignments() + .returning(|_, _, _| Ok(())); + token_mock + .expect_expand_project_information() + .returning(|_, _, _| Ok(())); + token_mock + .expect_expand_domain_information() + .returning(|_, _, _| Ok(())); token_mock .expect_encode_token() .returning(|_| Ok("token".to_string())); diff --git a/src/assignment/error.rs b/src/assignment/error.rs index 7d364cc8..7137af0d 100644 --- a/src/assignment/error.rs +++ b/src/assignment/error.rs @@ -55,6 +55,12 @@ pub enum AssignmentProviderError { source: RoleAssignmentListForMultipleActorTargetParametersBuilderError, }, + #[error("building role assignment query: {}", source)] + RoleAssignmentListParametersBuilder { + #[from] + source: RoleAssignmentListParametersBuilderError, + }, + #[error("building role data: {}", source)] RoleBuilderError { #[from] diff --git a/src/token/application_credential.rs b/src/token/application_credential.rs index 969d8a6b..cef2417d 100644 --- a/src/token/application_credential.rs +++ b/src/token/application_credential.rs @@ -18,6 +18,8 @@ use rmp::{decode::read_pfix, encode::write_pfix}; use std::collections::BTreeMap; use std::io::Write; +use crate::assignment::types::Role; +use crate::resource::types::Project; use crate::token::{ error::TokenProviderError, fernet::{self, MsgPackToken}, @@ -35,6 +37,11 @@ pub struct ApplicationCredentialToken { pub expires_at: DateTime, pub project_id: String, pub application_credential_id: String, + + #[builder(default)] + pub roles: Vec, + #[builder(default)] + pub project: Option, } impl ApplicationCredentialTokenBuilder { @@ -110,6 +117,7 @@ impl MsgPackToken for ApplicationCredentialToken { audit_ids, project_id, application_credential_id, + ..Default::default() }) } } @@ -130,6 +138,7 @@ mod tests { application_credential_id: Uuid::new_v4().simple().to_string(), audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() }; let auth_map = BTreeMap::from([(1, "password".into())]); let mut buf = vec![]; diff --git a/src/token/domain_scoped.rs b/src/token/domain_scoped.rs index 3e3050fd..3d1284c9 100644 --- a/src/token/domain_scoped.rs +++ b/src/token/domain_scoped.rs @@ -18,6 +18,8 @@ use rmp::{decode::read_pfix, encode::write_pfix}; use std::collections::BTreeMap; use std::io::Write; +use crate::assignment::types::Role; +use crate::resource::types::Domain; use crate::token::{ error::TokenProviderError, fernet::{self, MsgPackToken}, @@ -35,6 +37,11 @@ pub struct DomainScopeToken { pub audit_ids: Vec, pub expires_at: DateTime, pub domain_id: String, + + #[builder(default)] + pub roles: Vec, + #[builder(default)] + pub domain: Option, } impl DomainScopeTokenBuilder { @@ -106,6 +113,7 @@ impl MsgPackToken for DomainScopeToken { expires_at, audit_ids, domain_id, + ..Default::default() }) } } @@ -125,6 +133,7 @@ mod tests { domain_id: Uuid::new_v4().simple().to_string(), audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() }; let auth_map = BTreeMap::from([(1, "password".into())]); let mut buf = vec![]; diff --git a/src/token/error.rs b/src/token/error.rs index a1f86772..b6e68e55 100644 --- a/src/token/error.rs +++ b/src/token/error.rs @@ -115,4 +115,18 @@ pub enum TokenProviderError { #[from] source: crate::token::domain_scoped::DomainScopeTokenBuilderError, }, + + #[error(transparent)] + AssignmentProvider { + /// The source of the error. + #[from] + source: crate::assignment::error::AssignmentProviderError, + }, + + #[error(transparent)] + ResourceProvider { + /// The source of the error. + #[from] + source: crate::resource::error::ResourceProviderError, + }, } diff --git a/src/token/fernet.rs b/src/token/fernet.rs index 307cb76f..58604bad 100644 --- a/src/token/fernet.rs +++ b/src/token/fernet.rs @@ -336,6 +336,7 @@ pub(super) mod tests { domain_id: Uuid::new_v4().simple().to_string(), audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() }); let mut backend = FernetTokenProvider::default(); @@ -379,6 +380,7 @@ pub(super) mod tests { project_id: Uuid::new_v4().simple().to_string(), audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() }); let mut backend = FernetTokenProvider::default(); @@ -427,6 +429,7 @@ pub(super) mod tests { application_credential_id: Uuid::new_v4().simple().to_string(), audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() }); let mut backend = FernetTokenProvider::default(); diff --git a/src/token/mod.rs b/src/token/mod.rs index 464eb4fe..97b94bb6 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -16,6 +16,7 @@ use async_trait::async_trait; use chrono::{Local, TimeDelta}; #[cfg(test)] use mockall::mock; +use sea_orm::DatabaseConnection; pub mod application_credential; pub mod domain_scoped; @@ -26,8 +27,17 @@ pub mod project_scoped; pub mod types; pub mod unscoped; +use crate::assignment::{ + AssignmentApi, + error::AssignmentProviderError, + types::{Role, RoleAssignmentListParametersBuilder}, +}; use crate::config::{Config, TokenProvider as TokenProviderType}; -use crate::resource::types::{Domain, Project}; +use crate::provider::Provider; +use crate::resource::{ + ResourceApi, + types::{Domain, Project}, +}; pub use error::TokenProviderError; use types::TokenBackend; @@ -58,12 +68,14 @@ impl TokenProvider { #[async_trait] pub trait TokenApi: Send + Sync + Clone { + /// Validate the token async fn validate_token<'a>( &self, credential: &'a str, window_seconds: Option, ) -> Result; + /// Issue a token for given parameters fn issue_token( &self, user_id: U, @@ -75,7 +87,32 @@ pub trait TokenApi: Send + Sync + Clone { where U: AsRef; + /// Encode the token into the X-SubjectToken String fn encode_token(&self, token: &Token) -> Result; + + /// Populate role assignments in the token that support that information + async fn populate_role_assignments( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; + + /// Populate Project information in the token that support that information + async fn expand_project_information( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; + + /// Populate Domain information in the token that support that information + async fn expand_domain_information( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; } #[async_trait] @@ -110,7 +147,7 @@ impl TokenApi for TokenProvider { where U: AsRef, { - let token = if let Some(project) = &project { + let token = if let Some(project) = project { Token::ProjectScope( ProjectScopeTokenBuilder::default() .user_id(user_id.as_ref()) @@ -125,9 +162,10 @@ impl TokenApi for TokenProvider { .ok_or(TokenProviderError::ExpiryCalculation)?, ) .project_id(project.id.clone()) + .project(project.clone()) .build()?, ) - } else if let Some(domain) = &domain { + } else if let Some(domain) = domain { Token::DomainScope( DomainScopeTokenBuilder::default() .user_id(user_id.as_ref()) @@ -142,6 +180,7 @@ impl TokenApi for TokenProvider { .ok_or(TokenProviderError::ExpiryCalculation)?, ) .domain_id(domain.id.clone()) + .domain(domain.clone()) .build()?, ) } else { @@ -168,6 +207,137 @@ impl TokenApi for TokenProvider { fn encode_token(&self, token: &Token) -> Result { self.backend_driver.encode(token) } + + /// Populate role assignments in the token that support that information + async fn populate_role_assignments( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError> { + match token { + Token::ProjectScope(data) => { + let token_roles = provider + .get_assignment_provider() + .list_role_assignments( + db, + provider, + &RoleAssignmentListParametersBuilder::default() + .user_id(&data.user_id) + .project_id(&data.project_id) + .build() + .map_err(AssignmentProviderError::from)?, + ) + .await?; + data.roles = token_roles + .into_iter() + .map(|x| Role { + id: x.role_id.clone(), + name: x.role_name.clone().unwrap_or_default(), + ..Default::default() + }) + .collect::>(); + } + Token::DomainScope(data) => { + let token_roles = provider + .get_assignment_provider() + .list_role_assignments( + db, + provider, + &RoleAssignmentListParametersBuilder::default() + .user_id(&data.user_id) + .domain_id(&data.domain_id) + .build() + .map_err(AssignmentProviderError::from)?, + ) + .await?; + data.roles = token_roles + .into_iter() + .map(|x| Role { + id: x.role_id.clone(), + name: x.role_name.clone().unwrap_or_default(), + ..Default::default() + }) + .collect::>(); + } + Token::ApplicationCredential(data) => { + let token_roles = provider + .get_assignment_provider() + .list_role_assignments( + db, + provider, + &RoleAssignmentListParametersBuilder::default() + .user_id(&data.user_id) + .project_id(&data.project_id) + .build() + .map_err(AssignmentProviderError::from)?, + ) + .await?; + data.roles = token_roles + .into_iter() + .map(|x| Role { + id: x.role_id.clone(), + name: x.role_name.clone().unwrap_or_default(), + ..Default::default() + }) + .collect::>(); + } + _ => {} + } + + Ok(()) + } + + async fn expand_project_information( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError> { + match token { + Token::ProjectScope(data) => { + if data.project.is_none() { + let project = provider + .get_resource_provider() + .get_project(db, &data.project_id) + .await?; + + data.project = project; + } + } + Token::ApplicationCredential(data) => { + if data.project.is_none() { + let project = provider + .get_resource_provider() + .get_project(db, &data.project_id) + .await?; + + data.project = project; + } + } + _ => {} + }; + Ok(()) + } + + async fn expand_domain_information( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError> { + if let Token::DomainScope(data) = token { + if data.domain.is_none() { + let domain = provider + .get_resource_provider() + .get_domain(db, &data.domain_id) + .await?; + + data.domain = domain; + } + }; + Ok(()) + } } #[cfg(test)] @@ -197,6 +367,28 @@ mock! { U: AsRef; fn encode_token(&self, token: &Token) -> Result; + + async fn populate_role_assignments( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; + + async fn expand_project_information( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; + + async fn expand_domain_information( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; + } impl Clone for TokenProvider { @@ -204,3 +396,128 @@ mock! { } } + +#[cfg(test)] +mod tests { + use sea_orm::DatabaseConnection; + + use super::*; + use crate::assignment::{ + MockAssignmentProvider, + types::{Assignment, AssignmentType, Role, RoleAssignmentListParameters}, + }; + use crate::config::Config; + use crate::identity::MockIdentityProvider; + + use crate::provider::ProviderBuilder; + use crate::resource::MockResourceProvider; + use crate::token::{ + DomainScopeToken, MockTokenProvider, ProjectScopeToken, Token, UnscopedToken, + }; + + #[tokio::test] + async fn test_populate_role_assignments() { + let config = Config::default(); + let token_provider = TokenProvider::new(&config).unwrap(); + let db = DatabaseConnection::Disconnected; + let identity_mock = MockIdentityProvider::default(); + let resource_mock = MockResourceProvider::default(); + let token_mock = MockTokenProvider::default(); + let mut assignment_mock = MockAssignmentProvider::default(); + assignment_mock + .expect_list_role_assignments() + .withf(|_, _, q: &RoleAssignmentListParameters| { + q.project_id == Some("project_id".to_string()) + }) + .returning(|_, _, q: &RoleAssignmentListParameters| { + Ok(vec![Assignment { + role_id: "rid".into(), + role_name: Some("role_name".into()), + actor_id: q.user_id.clone().unwrap(), + target_id: q.project_id.clone().unwrap(), + r#type: AssignmentType::UserProject, + inherited: false, + }]) + }); + assignment_mock + .expect_list_role_assignments() + .withf(|_, _, q: &RoleAssignmentListParameters| { + q.domain_id == Some("domain_id".to_string()) + }) + .returning(|_, _, q: &RoleAssignmentListParameters| { + Ok(vec![Assignment { + role_id: "rid".into(), + role_name: Some("role_name".into()), + actor_id: q.user_id.clone().unwrap(), + target_id: q.domain_id.clone().unwrap(), + r#type: AssignmentType::UserProject, + inherited: false, + }]) + }); + let provider = ProviderBuilder::default() + .config(config.clone()) + .assignment(assignment_mock) + .identity(identity_mock) + .resource(resource_mock) + .token(token_mock) + .build() + .unwrap(); + + let mut ptoken = Token::ProjectScope(ProjectScopeToken { + user_id: "bar".into(), + project_id: "project_id".into(), + ..Default::default() + }); + token_provider + .populate_role_assignments(&mut ptoken, &db, &provider) + .await + .unwrap(); + + if let Token::ProjectScope(data) = ptoken { + assert_eq!( + data.roles, + vec![Role { + id: "rid".into(), + name: "role_name".into(), + ..Default::default() + }] + ); + } else { + panic!("Not project scope"); + } + + let mut dtoken = Token::DomainScope(DomainScopeToken { + user_id: "bar".into(), + domain_id: "domain_id".into(), + ..Default::default() + }); + token_provider + .populate_role_assignments(&mut dtoken, &db, &provider) + .await + .unwrap(); + + if let Token::DomainScope(data) = dtoken { + assert_eq!( + data.roles, + vec![Role { + id: "rid".into(), + name: "role_name".into(), + ..Default::default() + }] + ); + } else { + panic!("Not domain scope"); + } + + let mut utoken = Token::Unscoped(UnscopedToken { + user_id: "bar".into(), + ..Default::default() + }); + assert!( + token_provider + .populate_role_assignments(&mut utoken, &db, &provider) + .await + .is_ok() + ); + } +} diff --git a/src/token/project_scoped.rs b/src/token/project_scoped.rs index c0b69f57..74d27b2c 100644 --- a/src/token/project_scoped.rs +++ b/src/token/project_scoped.rs @@ -18,6 +18,8 @@ use rmp::{decode::read_pfix, encode::write_pfix}; use std::collections::BTreeMap; use std::io::Write; +use crate::assignment::types::Role; +use crate::resource::types::Project; use crate::token::{ error::TokenProviderError, fernet::{self, MsgPackToken}, @@ -35,6 +37,11 @@ pub struct ProjectScopeToken { pub audit_ids: Vec, pub expires_at: DateTime, pub project_id: String, + + #[builder(default)] + pub roles: Vec, + #[builder(default)] + pub project: Option, } impl ProjectScopeTokenBuilder { @@ -106,6 +113,7 @@ impl MsgPackToken for ProjectScopeToken { expires_at, audit_ids, project_id, + ..Default::default() }) } } @@ -125,6 +133,7 @@ mod tests { project_id: Uuid::new_v4().simple().to_string(), audit_ids: vec!["Zm9vCg".into()], expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() }; let auth_map = BTreeMap::from([(1, "password".into())]); let mut buf = vec![]; diff --git a/typos.toml b/typos.toml index 8ebeb37a..0f47f48d 100644 --- a/typos.toml +++ b/typos.toml @@ -28,7 +28,7 @@ ratatui = "ratatui" [type.rust] extend-glob = [] extend-ignore-identifiers-re = [] -extend-ignore-words-re = [] +extend-ignore-words-re = ["udid"] extend-ignore-re = [ "gAA.*\\b" ]