From 77cc378c4e3e5761aa0f894555944492471a9f88 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 7 Jan 2026 13:57:05 +0100 Subject: [PATCH] feat: Enable system_scope token support Add support to authorize with the system scope. Corresponding payload is added, as well as token validation. --- src/api/v3/auth/token/common.rs | 4 +- src/api/v3/auth/token/token_impl.rs | 44 ++++---- src/api/v3/auth/token/types.rs | 18 +++ src/api/v4/auth/token/token_impl.rs | 44 ++++---- src/api/v4/auth/token/types.rs | 21 ++++ src/assignment/mod.rs | 2 +- src/assignment/types/assignment.rs | 2 +- src/auth/mod.rs | 10 ++ src/token/backend/fernet.rs | 34 +++--- src/token/mod.rs | 143 ++++++++++++++++-------- src/token/types.rs | 15 +++ src/token/types/system_scoped.rs | 163 ++++++++++++++++++++++++++++ 12 files changed, 398 insertions(+), 102 deletions(-) create mode 100644 src/token/types/system_scoped.rs diff --git a/src/api/v3/auth/token/common.rs b/src/api/v3/auth/token/common.rs index 86303020..e9484767 100644 --- a/src/api/v3/auth/token/common.rs +++ b/src/api/v3/auth/token/common.rs @@ -131,9 +131,7 @@ pub(super) async fn get_authz_info( return Err(KeystoneApiError::Unauthorized(None)); } } - Some(Scope::System(_scope)) => { - todo!() - } + Some(Scope::System(_scope)) => AuthzInfo::System, None => AuthzInfo::Unscoped, }; authz_info.validate()?; diff --git a/src/api/v3/auth/token/token_impl.rs b/src/api/v3/auth/token/token_impl.rs index dc56cc51..51c6a140 100644 --- a/src/api/v3/auth/token/token_impl.rs +++ b/src/api/v3/auth/token/token_impl.rs @@ -14,7 +14,7 @@ use crate::api::common; use crate::api::error::KeystoneApiError; -use crate::api::v3::auth::token::types::{Token, TokenBuilder, UserBuilder}; +use crate::api::v3::auth::token::types::{System, Token, TokenBuilder, UserBuilder}; use crate::api::v3::role::types::Role; use crate::identity::IdentityApi; use crate::keystone::ServiceState; @@ -38,6 +38,7 @@ impl Token { response.audit_ids(token.audit_ids().clone()); response.methods(token.methods().clone()); response.expires_at(*token.expires_at()); + response.issued_at(*token.issued_at()); let user = if let Some(user) = token.user() { user @@ -75,15 +76,7 @@ impl Token { } match token { - ProviderToken::Unscoped(_token) => {} - ProviderToken::DomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } - } - ProviderToken::ProjectScope(token) => { + ProviderToken::ApplicationCredential(token) => { if project.is_none() { project = Some( state @@ -98,7 +91,22 @@ impl Token { ); } } - ProviderToken::ApplicationCredential(token) => { + ProviderToken::DomainScope(token) => { + if domain.is_none() { + domain = Some( + common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, + ); + } + } + ProviderToken::FederationUnscoped(_token) => {} + ProviderToken::FederationDomainScope(token) => { + if domain.is_none() { + domain = Some( + common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, + ); + } + } + ProviderToken::FederationProjectScope(token) => { if project.is_none() { project = Some( state @@ -113,15 +121,7 @@ impl Token { ); } } - ProviderToken::FederationUnscoped(_token) => {} - ProviderToken::FederationDomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } - } - ProviderToken::FederationProjectScope(token) => { + ProviderToken::ProjectScope(token) => { if project.is_none() { project = Some( state @@ -151,6 +151,9 @@ impl Token { ); } } + ProviderToken::SystemScope(_token) => { + response.system(System { all: true }); + } ProviderToken::Trust(token) => { if project.is_none() { project = Some( @@ -182,6 +185,7 @@ impl Token { ); } } + ProviderToken::Unscoped(_token) => {} } if let Some(domain) = domain { diff --git a/src/api/v3/auth/token/types.rs b/src/api/v3/auth/token/types.rs index e56368f9..37efe59c 100644 --- a/src/api/v3/auth/token/types.rs +++ b/src/api/v3/auth/token/types.rs @@ -62,6 +62,9 @@ pub struct Token { /// The date and time when the token expires. pub expires_at: DateTime, + /// The date and time when the token was issued. + pub issued_at: DateTime, + // # Subject /// A user object. //#[builder(default)] @@ -98,6 +101,12 @@ pub struct Token { #[validate(nested)] pub roles: Option>, + /// A system object. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + #[validate(nested)] + pub system: Option, + /// A catalog object. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] @@ -286,3 +295,12 @@ pub struct ValidateTokenParameters { /// return a 404 exception. pub allow_expired: Option, } + +/// System information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct System { + /// All + pub all: bool, +} diff --git a/src/api/v4/auth/token/token_impl.rs b/src/api/v4/auth/token/token_impl.rs index 3d7a70c0..2a2b36b9 100644 --- a/src/api/v4/auth/token/token_impl.rs +++ b/src/api/v4/auth/token/token_impl.rs @@ -15,7 +15,7 @@ use crate::api::common; use crate::api::error::KeystoneApiError; use crate::api::v3::role::types::Role; -use crate::api::v4::auth::token::types::{Token, TokenBuilder, UserBuilder}; +use crate::api::v4::auth::token::types::{System, Token, TokenBuilder, UserBuilder}; use crate::identity::IdentityApi; use crate::keystone::ServiceState; use crate::resource::{ @@ -37,6 +37,7 @@ impl Token { response.audit_ids(token.audit_ids().clone()); response.methods(token.methods().clone()); response.expires_at(*token.expires_at()); + response.issued_at(*token.issued_at()); let user = if let Some(user) = token.user() { user @@ -74,15 +75,7 @@ impl Token { } match token { - ProviderToken::Unscoped(_token) => {} - ProviderToken::DomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } - } - ProviderToken::ProjectScope(token) => { + ProviderToken::ApplicationCredential(token) => { if project.is_none() { project = Some( state @@ -97,7 +90,22 @@ impl Token { ); } } - ProviderToken::ApplicationCredential(token) => { + ProviderToken::DomainScope(token) => { + if domain.is_none() { + domain = Some( + common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, + ); + } + } + ProviderToken::FederationUnscoped(_token) => {} + ProviderToken::FederationDomainScope(token) => { + if domain.is_none() { + domain = Some( + common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, + ); + } + } + ProviderToken::FederationProjectScope(token) => { if project.is_none() { project = Some( state @@ -112,15 +120,7 @@ impl Token { ); } } - ProviderToken::FederationUnscoped(_token) => {} - ProviderToken::FederationDomainScope(token) => { - if domain.is_none() { - domain = Some( - common::get_domain(state, Some(&token.domain_id), None::<&str>).await?, - ); - } - } - ProviderToken::FederationProjectScope(token) => { + ProviderToken::ProjectScope(token) => { if project.is_none() { project = Some( state @@ -150,6 +150,9 @@ impl Token { ); } } + ProviderToken::SystemScope(_token) => { + response.system(System { all: true }); + } ProviderToken::Trust(token) => { if project.is_none() { project = Some( @@ -169,6 +172,7 @@ impl Token { response.trust(trust); } } + ProviderToken::Unscoped(_token) => {} } if let Some(domain) = domain { diff --git a/src/api/v4/auth/token/types.rs b/src/api/v4/auth/token/types.rs index 5959ae54..5d10734c 100644 --- a/src/api/v4/auth/token/types.rs +++ b/src/api/v4/auth/token/types.rs @@ -62,11 +62,16 @@ pub struct Token { /// The date and time when the token expires. pub expires_at: DateTime, + /// The date and time when the token was issued. + pub issued_at: DateTime, + + // # Subject /// A user object. //#[builder(default)] #[validate(nested)] pub user: User, + // # Scope /// A domain object including the id and name representing the domain the /// token is scoped to. This is only included in tokens that are scoped /// to a domain. @@ -83,12 +88,19 @@ pub struct Token { #[validate(nested)] pub project: Option, + /// A system object. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + #[validate(nested)] + pub system: Option, + /// A trust object. #[serde(skip_serializing_if = "Option::is_none", rename = "OS-TRUST:trust")] #[builder(default)] #[validate(nested)] pub trust: Option, + // # Roles on the scope. /// A list of role objects #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] @@ -275,3 +287,12 @@ pub struct ValidateTokenParameters { /// return a 404 exception. pub allow_expired: Option, } + +/// System information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct System { + /// All + pub all: bool, +} diff --git a/src/assignment/mod.rs b/src/assignment/mod.rs index 7abc5092..aae1debd 100644 --- a/src/assignment/mod.rs +++ b/src/assignment/mod.rs @@ -217,7 +217,7 @@ impl AssignmentApi for AssignmentProvider { r#type: RoleAssignmentTargetType::Domain, inherited: Some(false), }); - } else if let Some(val) = ¶ms.system { + } else if let Some(val) = ¶ms.system_id { targets.push(RoleAssignmentTarget { id: val.clone(), r#type: RoleAssignmentTargetType::System, diff --git a/src/assignment/types/assignment.rs b/src/assignment/types/assignment.rs index 3f539aa6..2a08aa1c 100644 --- a/src/assignment/types/assignment.rs +++ b/src/assignment/types/assignment.rs @@ -230,7 +230,7 @@ pub struct RoleAssignmentListParameters { /// Query role assignments on the system. #[builder(default)] #[validate(length(max = 64))] - pub system: Option, + pub system_id: Option, // #[builder(default)] // pub inherited: Option, diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 81039561..dfe2df2b 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -156,6 +156,8 @@ pub enum AuthzInfo { Domain(Domain), /// Project scope. Project(Project), + /// System scope. + System, /// Trust scope. Trust(Trust), /// Unscoped. @@ -180,6 +182,7 @@ impl AuthzInfo { return Err(AuthenticationError::Unauthorized); } } + AuthzInfo::System => {} AuthzInfo::Trust(_) => {} AuthzInfo::Unscoped => {} } @@ -295,6 +298,13 @@ mod tests { } } + #[test] + #[traced_test] + fn test_authz_validate_system() { + let authz = AuthzInfo::System; + assert!(authz.validate().is_ok()); + } + #[test] #[traced_test] fn test_authz_validate_unscoped() { diff --git a/src/token/backend/fernet.rs b/src/token/backend/fernet.rs index 58c02c20..0cc4dbca 100644 --- a/src/token/backend/fernet.rs +++ b/src/token/backend/fernet.rs @@ -221,6 +221,7 @@ impl FernetTokenProvider { 4 => Ok(FederationUnscopedPayload::disassemble(rd, self)?.into()), 5 => Ok(FederationProjectScopePayload::disassemble(rd, self)?.into()), 6 => Ok(FederationDomainScopePayload::disassemble(rd, self)?.into()), + 8 => Ok(SystemScopePayload::disassemble(rd, self)?.into()), 9 => Ok(ApplicationCredentialPayload::disassemble(rd, self)?.into()), 11 => Ok(RestrictedPayload::disassemble(rd, self)?.into()), other => Err(TokenProviderError::InvalidTokenType(other)), @@ -237,10 +238,10 @@ impl FernetTokenProvider { token.validate()?; let mut buf = vec![]; match token { - Token::Unscoped(data) => { - write_array_len(&mut buf, 5) + Token::ApplicationCredential(data) => { + write_array_len(&mut buf, 7) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - write_pfix(&mut buf, 0) + write_pfix(&mut buf, 9) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; data.assemble(&mut buf, self)?; } @@ -251,13 +252,6 @@ impl FernetTokenProvider { .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; data.assemble(&mut buf, self)?; } - 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)?; - } Token::Trust(data) => { write_array_len(&mut buf, 7) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; @@ -286,10 +280,10 @@ impl FernetTokenProvider { .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; data.assemble(&mut buf, self)?; } - Token::ApplicationCredential(data) => { - write_array_len(&mut buf, 7) + Token::ProjectScope(data) => { + write_array_len(&mut buf, 6) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - write_pfix(&mut buf, 9) + write_pfix(&mut buf, 2) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; data.assemble(&mut buf, self)?; } @@ -300,6 +294,20 @@ impl FernetTokenProvider { .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; data.assemble(&mut buf, self)?; } + Token::SystemScope(data) => { + write_array_len(&mut buf, 6) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + write_pfix(&mut buf, 8) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + data.assemble(&mut buf, self)?; + } + Token::Unscoped(data) => { + write_array_len(&mut buf, 5) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + write_pfix(&mut buf, 0) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + data.assemble(&mut buf, self)?; + } } Ok(buf.into()) } diff --git a/src/token/mod.rs b/src/token/mod.rs index a6cc00b2..bc541aed 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -294,6 +294,23 @@ impl TokenProvider { )) } + /// Create system scoped token. + fn create_system_scoped_token( + &self, + authentication_info: &AuthenticatedInfo, + ) -> Result { + Ok(Token::SystemScope( + SystemScopePayloadBuilder::default() + .user_id(authentication_info.user_id.clone()) + .user(authentication_info.user.clone()) + .methods(authentication_info.methods.clone().iter()) + .audit_ids(authentication_info.audit_ids.clone().iter()) + .system_id("system") + .expires_at(self.get_new_token_expiry()?) + .build()?, + )) + } + /// Create token based on the trust. fn create_trust_token( &self, @@ -363,6 +380,9 @@ impl TokenProvider { Token::Restricted(data) => { data.user = user; } + Token::SystemScope(data) => { + data.user = user; + } Token::Trust(data) => { data.user = if let Some(trust) = &data.trust && trust.impersonation @@ -468,6 +488,7 @@ impl TokenProvider { data.project = project; } } + Token::SystemScope(_data) => {} Token::Trust(data) => { if data.trust.is_none() { data.trust = state @@ -497,7 +518,47 @@ impl TokenProvider { token: &mut Token, ) -> Result<(), TokenProviderError> { match token { - Token::ProjectScope(data) => { + Token::ApplicationCredential(data) => { + if data.application_credential.is_none() { + data.application_credential = Some( + state + .provider + .get_application_credential_provider() + .get_application_credential(state, &data.application_credential_id) + .await? + .ok_or_else(|| { + TokenProviderError::ApplicationCredentialNotFound( + data.application_credential_id.clone(), + ) + })?, + ); + } + if let Some(ref mut ac) = data.application_credential { + let user_role_ids: HashSet = state + .provider + .get_assignment_provider() + .list_role_assignments( + state, + &RoleAssignmentListParametersBuilder::default() + .user_id(&data.user_id) + .project_id(&ac.project_id) + .include_names(false) + .effective(true) + .build() + .map_err(AssignmentProviderError::from)?, + ) + .await? + .into_iter() + .map(|x| x.role_id.clone()) + .collect(); + // Filter out roles referred in the AC that the user does not have anymore. + ac.roles.retain(|role| user_role_ids.contains(&role.id)); + if ac.roles.is_empty() { + return Err(TokenProviderError::ActorHasNoRolesOnTarget); + } + }; + } + Token::DomainScope(data) => { data.roles = Some( state .provider @@ -506,7 +567,7 @@ impl TokenProvider { state, &RoleAssignmentListParametersBuilder::default() .user_id(&data.user_id) - .project_id(&data.project_id) + .domain_id(&data.domain_id) .include_names(true) .effective(true) .build() @@ -525,7 +586,7 @@ impl TokenProvider { return Err(TokenProviderError::ActorHasNoRolesOnTarget); } } - Token::DomainScope(data) => { + Token::FederationProjectScope(data) => { data.roles = Some( state .provider @@ -534,7 +595,7 @@ impl TokenProvider { state, &RoleAssignmentListParametersBuilder::default() .user_id(&data.user_id) - .domain_id(&data.domain_id) + .project_id(&data.project_id) .include_names(true) .effective(true) .build() @@ -553,47 +614,35 @@ impl TokenProvider { return Err(TokenProviderError::ActorHasNoRolesOnTarget); } } - Token::ApplicationCredential(data) => { - if data.application_credential.is_none() { - data.application_credential = Some( - state - .provider - .get_application_credential_provider() - .get_application_credential(state, &data.application_credential_id) - .await? - .ok_or_else(|| { - TokenProviderError::ApplicationCredentialNotFound( - data.application_credential_id.clone(), - ) - })?, - ); - } - if let Some(ref mut ac) = data.application_credential { - let user_role_ids: HashSet = state + Token::FederationDomainScope(data) => { + data.roles = Some( + state .provider .get_assignment_provider() .list_role_assignments( state, &RoleAssignmentListParametersBuilder::default() .user_id(&data.user_id) - .project_id(&ac.project_id) - .include_names(false) + .domain_id(&data.domain_id) + .include_names(true) .effective(true) .build() .map_err(AssignmentProviderError::from)?, ) .await? .into_iter() - .map(|x| x.role_id.clone()) - .collect(); - // Filter out roles referred in the AC that the user does not have anymore. - ac.roles.retain(|role| user_role_ids.contains(&role.id)); - if ac.roles.is_empty() { - return Err(TokenProviderError::ActorHasNoRolesOnTarget); - } - }; + .map(|x| Role { + id: x.role_id.clone(), + name: x.role_name.clone().unwrap_or_default(), + ..Default::default() + }) + .collect(), + ); + if data.roles.as_ref().is_none_or(|roles| roles.is_empty()) { + return Err(TokenProviderError::ActorHasNoRolesOnTarget); + } } - Token::FederationProjectScope(data) => { + Token::ProjectScope(data) => { data.roles = Some( state .provider @@ -621,7 +670,17 @@ impl TokenProvider { return Err(TokenProviderError::ActorHasNoRolesOnTarget); } } - Token::FederationDomainScope(data) => { + Token::Restricted(data) => { + if data.roles.is_none() { + self.get_token_restriction(state, &data.token_restriction_id, true) + .await? + .inspect(|restrictions| data.roles = restrictions.roles.clone()) + .ok_or(TokenProviderError::TokenRestrictionNotFound( + data.token_restriction_id.clone(), + ))?; + } + } + Token::SystemScope(data) => { data.roles = Some( state .provider @@ -630,7 +689,7 @@ impl TokenProvider { state, &RoleAssignmentListParametersBuilder::default() .user_id(&data.user_id) - .domain_id(&data.domain_id) + .system_id(&data.system_id) .include_names(true) .effective(true) .build() @@ -649,16 +708,6 @@ impl TokenProvider { return Err(TokenProviderError::ActorHasNoRolesOnTarget); } } - Token::Restricted(data) => { - if data.roles.is_none() { - self.get_token_restriction(state, &data.token_restriction_id, true) - .await? - .inspect(|restrictions| data.roles = restrictions.roles.clone()) - .ok_or(TokenProviderError::TokenRestrictionNotFound( - data.token_restriction_id.clone(), - ))?; - } - } Token::Trust(data) => { // Resolve role assignments of the trust verifying that the trustor still has // those roles on the scope. @@ -817,6 +866,11 @@ impl TokenApi for TokenProvider { message: "cannot create trust token with an identity provider in scope".into(), context: "issuing token".into(), }), + AuthzInfo::System => Err(TokenProviderError::Conflict { + message: "cannot create system scope token with an identity provider in scope" + .into(), + context: "issuing token".into(), + }), AuthzInfo::Unscoped => self.create_federated_unscoped_token(&authentication_info), } } else { @@ -828,6 +882,7 @@ impl TokenApi for TokenProvider { self.create_project_scope_token(&authentication_info, project) } AuthzInfo::Trust(trust) => self.create_trust_token(&authentication_info, trust), + AuthzInfo::System => self.create_system_scoped_token(&authentication_info), AuthzInfo::Unscoped => self.create_unscoped_token(&authentication_info), } } diff --git a/src/token/types.rs b/src/token/types.rs index 8e250c4d..123376c9 100644 --- a/src/token/types.rs +++ b/src/token/types.rs @@ -33,6 +33,7 @@ pub mod federation_unscoped; pub mod project_scoped; pub mod provider_api; pub mod restricted; +pub mod system_scoped; pub mod trust; pub mod unscoped; @@ -48,6 +49,7 @@ pub use federation_unscoped::{FederationUnscopedPayload, FederationUnscopedPaylo pub use project_scoped::{ProjectScopePayload, ProjectScopePayloadBuilder}; pub use provider_api::TokenApi; pub use restricted::*; +pub use system_scoped::*; pub use trust::*; pub use unscoped::*; @@ -69,6 +71,8 @@ pub enum Token { ProjectScope(ProjectScopePayload), /// Restricted. Restricted(RestrictedPayload), + /// System scoped. + SystemScope(SystemScopePayload), /// Trust. Trust(TrustPayload), /// Unscoped. @@ -85,6 +89,7 @@ impl Token { Self::FederationDomainScope(x) => &x.user_id, Self::ProjectScope(x) => &x.user_id, Self::Restricted(x) => &x.user_id, + Self::SystemScope(x) => &x.user_id, Self::Trust(x) => &x.user_id, Self::Unscoped(x) => &x.user_id, } @@ -99,6 +104,7 @@ impl Token { Self::FederationDomainScope(x) => &x.user, Self::ProjectScope(x) => &x.user, Self::Restricted(x) => &x.user, + Self::SystemScope(x) => &x.user, Self::Trust(x) => &x.user, Self::Unscoped(x) => &x.user, } @@ -117,6 +123,7 @@ impl Token { Self::FederationDomainScope(x) => x.issued_at = issued_at, Self::ProjectScope(x) => x.issued_at = issued_at, Self::Restricted(x) => x.issued_at = issued_at, + Self::SystemScope(x) => x.issued_at = issued_at, Self::Trust(x) => x.issued_at = issued_at, Self::Unscoped(x) => x.issued_at = issued_at, } @@ -136,6 +143,7 @@ impl Token { Self::FederationDomainScope(x) => &x.issued_at, Self::ProjectScope(x) => &x.issued_at, Self::Restricted(x) => &x.issued_at, + Self::SystemScope(x) => &x.issued_at, Self::Trust(x) => &x.issued_at, Self::Unscoped(x) => &x.issued_at, } @@ -151,6 +159,7 @@ impl Token { Self::FederationDomainScope(x) => &x.expires_at, Self::ProjectScope(x) => &x.expires_at, Self::Restricted(x) => &x.expires_at, + Self::SystemScope(x) => &x.expires_at, Self::Trust(x) => &x.expires_at, Self::Unscoped(x) => &x.expires_at, } @@ -165,6 +174,7 @@ impl Token { Self::FederationDomainScope(x) => &x.methods, Self::ProjectScope(x) => &x.methods, Self::Restricted(x) => &x.methods, + Self::SystemScope(x) => &x.methods, Self::Trust(x) => &x.methods, Self::Unscoped(x) => &x.methods, } @@ -179,6 +189,7 @@ impl Token { Self::FederationDomainScope(x) => &x.audit_ids, Self::ProjectScope(x) => &x.audit_ids, Self::Restricted(x) => &x.audit_ids, + Self::SystemScope(x) => &x.audit_ids, Self::Trust(x) => &x.audit_ids, Self::Unscoped(x) => &x.audit_ids, } @@ -225,6 +236,7 @@ impl Token { Self::FederationDomainScope(x) => x.roles.as_ref(), Self::ProjectScope(x) => x.roles.as_ref(), Self::Restricted(x) => x.roles.as_ref(), + Self::SystemScope(x) => x.roles.as_ref(), Self::Trust(x) => match &x.trust { Some(trust) => trust.roles.as_ref(), None => None, @@ -308,6 +320,7 @@ impl Token { return Err(TokenProviderError::ProjectDisabled(data.project_id.clone())); } } + Token::SystemScope(_data) => {} Token::Trust(data) => { if !data .project @@ -355,6 +368,7 @@ impl Token { Token::FederationUnscoped(_data) => {} Token::ProjectScope(_data) => {} Token::Restricted(_data) => {} + Token::SystemScope(_data) => {} Token::Trust(data) => { state .provider @@ -383,6 +397,7 @@ impl Validate for Token { Self::FederationDomainScope(x) => x.validate(), Self::ProjectScope(x) => x.validate(), Self::Restricted(x) => x.validate(), + Self::SystemScope(x) => x.validate(), Self::Trust(x) => x.validate(), Self::Unscoped(x) => x.validate(), } diff --git a/src/token/types/system_scoped.rs b/src/token/types/system_scoped.rs new file mode 100644 index 00000000..057477f6 --- /dev/null +++ b/src/token/types/system_scoped.rs @@ -0,0 +1,163 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use rmp::{decode::read_pfix, encode::write_pfix}; +use serde::Serialize; +use std::io::Write; +use validator::Validate; + +use crate::assignment::types::Role; +use crate::error::BuilderError; +use crate::identity::types::UserResponse; +use crate::token::types::common; +use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, + error::TokenProviderError, + types::Token, +}; + +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into))] +pub struct SystemScopePayload { + #[validate(length(min = 1, max = 64))] + pub user_id: String, + + #[builder(default, setter(name = _methods))] + #[validate(length(min = 1))] + pub methods: Vec, + + #[builder(default, setter(name = _audit_ids))] + #[validate(custom(function = "common::validate_audit_ids"))] + pub audit_ids: Vec, + pub expires_at: DateTime, + + #[validate(length(min = 1, max = 64))] + pub system_id: String, + + #[builder(default)] + pub issued_at: DateTime, + + #[builder(default)] + pub user: Option, + #[builder(default)] + pub roles: Option>, +} + +impl SystemScopePayloadBuilder { + 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: SystemScopePayload) -> Self { + Self::SystemScope(value) + } +} + +impl MsgPackToken for SystemScopePayload { + type Token = Self; + + fn assemble( + &self, + wd: &mut W, + fernet_provider: &FernetTokenProvider, + ) -> Result<(), TokenProviderError> { + utils::write_uuid(wd, &self.user_id)?; + write_pfix( + wd, + fernet_provider.encode_auth_methods(self.methods.clone())?, + ) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + utils::write_str(wd, &self.system_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; + + Ok(()) + } + + fn disassemble( + rd: &mut &[u8], + fernet_provider: &FernetTokenProvider, + ) -> Result { + // Order of reading is important + let user_id = utils::read_uuid(rd)?; + let methods: Vec = fernet_provider + .decode_auth_methods(read_pfix(rd)?)? + .into_iter() + .collect(); + let system_id = utils::read_str(rd)?; + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); + Ok(Self { + user_id, + methods, + expires_at, + audit_ids, + system_id, + ..Default::default() + }) + } +} + +#[cfg(test)] +mod tests { + use chrono::{Local, SubsecRound}; + use uuid::Uuid; + + use super::*; + use crate::token::tests::setup_config; + + #[test] + fn test_roundtrip() { + let token = SystemScopePayload { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + system_id: "system".to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() + }; + + let provider = FernetTokenProvider::new(setup_config()); + + let mut buf = vec![]; + token.assemble(&mut buf, &provider).unwrap(); + let encoded_buf = buf.clone(); + let decoded = + SystemScopePayload::disassemble(&mut encoded_buf.as_slice(), &provider).unwrap(); + assert_eq!(token, decoded); + } +}