diff --git a/Cargo.lock b/Cargo.lock index a0afaf94..ab4f4bd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5871,6 +5871,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -7227,6 +7233,7 @@ dependencies = [ "getrandom 0.4.2", "js-sys", "serde_core", + "sha1_smol", "wasm-bindgen", ] diff --git a/crates/api-types/src/error_conv.rs b/crates/api-types/src/error_conv.rs index 31490070..e31b4b3a 100644 --- a/crates/api-types/src/error_conv.rs +++ b/crates/api-types/src/error_conv.rs @@ -75,12 +75,18 @@ impl From for KeystoneApiError { impl From for KeystoneApiError { fn from(value: AuthenticationError) -> Self { match value { + AuthenticationError::AuthPrincipalDiffers => { + KeystoneApiError::unauthorized(value, None::) + } AuthenticationError::DomainDisabled(..) => { KeystoneApiError::unauthorized(value, None::) } AuthenticationError::ProjectDisabled(..) => { KeystoneApiError::unauthorized(value, None::) } + AuthenticationError::ScopeNotAllowed => { + KeystoneApiError::unauthorized(value, None::) + } AuthenticationError::StructBuilder { source } => { KeystoneApiError::InternalError(source.to_string()) } @@ -117,6 +123,9 @@ impl From for KeystoneApiError { AuthenticationError::Unauthorized => { KeystoneApiError::unauthorized(value, None::) } + AuthenticationError::Validation { source } => { + KeystoneApiError::InternalError(source.to_string()) + } } } } diff --git a/crates/appcred-sql/src/application_credential/create.rs b/crates/appcred-sql/src/application_credential/create.rs index e3237d60..7dcba4f3 100644 --- a/crates/appcred-sql/src/application_credential/create.rs +++ b/crates/appcred-sql/src/application_credential/create.rs @@ -122,7 +122,7 @@ pub async fn create( // Process access rules if let Some(access_rules) = rec.access_rules { builder.access_rules( - process_access_rules(&txn, access_rules.into_iter(), internal_id, rec.user_id) + process_access_rules(&txn, access_rules, internal_id, rec.user_id) .await? .into_iter() .collect::>(), diff --git a/crates/appcred-sql/src/application_credential/list.rs b/crates/appcred-sql/src/application_credential/list.rs index 5ac2da0a..0086a5e0 100644 --- a/crates/appcred-sql/src/application_credential/list.rs +++ b/crates/appcred-sql/src/application_credential/list.rs @@ -94,10 +94,7 @@ pub async fn list( }) .collect::>, _>>()?; let mut results: Vec = Vec::new(); - for (ref apc, (roles, rules)) in db_entities - .into_iter() - .zip(roles.into_iter().zip(rules.into_iter())) - { + for (ref apc, (roles, rules)) in db_entities.into_iter().zip(roles.into_iter().zip(rules)) { let mut builder: ApplicationCredentialBuilder = apc.try_into()?; builder.roles(roles); builder.access_rules(rules); diff --git a/crates/cli-manage/src/storage/demote.rs b/crates/cli-manage/src/storage/demote.rs index 09e2ff06..e972be73 100644 --- a/crates/cli-manage/src/storage/demote.rs +++ b/crates/cli-manage/src/storage/demote.rs @@ -58,7 +58,7 @@ impl PerformAction for DemoteCommand { client .change_membership(pb::raft::ChangeMembershipRequest { - members: Vec::from_iter(members.into_iter()), + members: Vec::from_iter(members), retain: true, }) .await?; diff --git a/crates/cli-manage/src/storage/promote.rs b/crates/cli-manage/src/storage/promote.rs index 0b33e967..8b214912 100644 --- a/crates/cli-manage/src/storage/promote.rs +++ b/crates/cli-manage/src/storage/promote.rs @@ -59,7 +59,7 @@ impl PerformAction for PromoteCommand { client .change_membership(pb::raft::ChangeMembershipRequest { - members: Vec::from_iter(members.into_iter()), + members: Vec::from_iter(members), retain: false, }) .await?; diff --git a/crates/cli-manage/src/storage/remove_peer.rs b/crates/cli-manage/src/storage/remove_peer.rs index 82472801..d0b3a257 100644 --- a/crates/cli-manage/src/storage/remove_peer.rs +++ b/crates/cli-manage/src/storage/remove_peer.rs @@ -60,7 +60,7 @@ impl PerformAction for RemovePeerCommand { client .change_membership(pb::raft::ChangeMembershipRequest { - members: Vec::from_iter(members.into_iter()), + members: Vec::from_iter(members), retain: false, }) .await?; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 0d959a63..6a744a5b 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -347,8 +347,8 @@ impl ConfigManager { // Watch the main config watched_paths.insert(config_path.clone()); if let Some(parent) = config_path.parent() { - // For K8 it is practical to add a directory watch since the CM is replaced as a whole - // without touching the individual file. + // For K8 it is practical to add a directory watch since the CM is replaced as a + // whole without touching the individual file. watched_paths.insert(parent.to_path_buf()); } diff --git a/crates/core-types/Cargo.toml b/crates/core-types/Cargo.toml index 6be29dd1..bc9085a7 100644 --- a/crates/core-types/Cargo.toml +++ b/crates/core-types/Cargo.toml @@ -19,7 +19,7 @@ serde_json.workspace = true thiserror.workspace = true tracing.workspace = true url = { workspace = true, features = ["serde"] } -uuid.workspace = true +uuid = { workspace = true, features = ["v4", "v5"] } validator = { workspace = true, features = ["derive"] } [dev-dependencies] diff --git a/crates/core-types/src/auth.rs b/crates/core-types/src/auth.rs index 6e5529e2..b872ba9a 100644 --- a/crates/core-types/src/auth.rs +++ b/crates/core-types/src/auth.rs @@ -20,20 +20,33 @@ //! present here to be shared across different authentication methods. The //! same is valid for the authorization validation (project/domain must exist //! and be enabled). +use std::collections::{HashMap, HashSet}; +use std::iter::once; +use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use chrono::{DateTime, Utc}; use derive_builder::Builder; use thiserror::Error; use tracing::warn; +use uuid::{Uuid, uuid}; +use validator::{Validate, ValidationErrors}; use crate::application_credential::ApplicationCredential; use crate::error::BuilderError; use crate::identity::{Group, UserResponse}; use crate::resource::{Domain, Project}; +use crate::token::TokenRestriction; use crate::trust::Trust; +/// Namespace UUID for the virtual ID generation based on the UUIDv5 +const NAMESPACE_UUID: Uuid = uuid!("96f0e3b8-0d21-41bc-bd0d-457da94345f9"); + #[derive(Error, Debug)] pub enum AuthenticationError { + /// Auth principal mismatch. + #[error("the principal differs in authentication results")] + AuthPrincipalDiffers, + /// Domain is disabled. #[error("The domain is disabled.")] DomainDisabled(String), @@ -42,6 +55,10 @@ pub enum AuthenticationError { #[error("The project is disabled.")] ProjectDisabled(String), + /// Scope is not allowed with the current SecurityContext. + #[error("target scope is not allowed with the current authentication context")] + ScopeNotAllowed, + /// Structures builder error. #[error(transparent)] StructBuilder { @@ -73,41 +90,314 @@ pub enum AuthenticationError { /// User password is expired. #[error("The password is expired for user: {0}")] UserPasswordExpired(String), + + /// Validation error. + #[error("context validation error")] + Validation { + /// The source of the error. + #[from] + source: ValidationErrors, + }, } -/// Information about successful authentication. -#[derive(Builder, Clone, Debug, Default, PartialEq)] +/// Security Context of the operation. +/// +/// Authentication and information bound to the operation. +#[derive(Builder, Clone, Debug, PartialEq)] #[builder(build_fn(error = "BuilderError"))] -#[builder(setter(into, strip_option))] -pub struct AuthenticatedInfo { - /// Application credential. - #[builder(default)] - pub application_credential: Option, - +#[builder(private, setter(into, strip_option))] +pub struct SecurityContext { /// Audit IDs. - #[builder(default)] pub audit_ids: Vec, - /// Authentication expiration. - #[builder(default)] - pub expires_at: Option>, + /// Authentication context (how the authentication was performed). + // TODO: It may be a Vec in the case of MFA + pub authentication_context: AuthenticationContext, - /// Federated IDP id. - #[builder(default)] - pub idp_id: Option, + /// Authentication methods used to establish the context. + pub auth_methods: HashSet, - /// Authentication methods. + /// Authorization scope of the context. During the authentication request + /// this information becomes available at the later phase. #[builder(default)] - pub methods: Vec, + pub authorization: Option, - /// Federated protocol id. + /// Authentication expiration. #[builder(default)] - pub protocol_id: Option, + pub expires_at: Option>, + + /// Identity information. + pub principal: PrincipalInfo, /// Token restriction. #[builder(default)] - pub token_restriction_id: Option, + pub token_restriction: Option, +} + +impl SecurityContext { + /// Validate the authentication information: + /// + /// - User attribute must be set + /// - User must be enabled + /// - User object id must match user_id + pub fn validate(&self) -> Result<(), AuthenticationError> { + self.principal.validate()?; + Ok(()) + } + + /// SECURITY GATE: Validate whether the scope is accessible with the current + /// [`SecurityContext`]. + /// + /// Perform validation whether it is possible to grant authorization for the + /// scope based on the authentication or whether it violates the bounds + /// of the current authentication. No check for whether the principal has + /// any roles on the target scope. + /// + /// # Security Notes + /// No validations of whether the principal has any roles on the target + /// scope are performed. This is barely AuthN/AuthZ context boundaries + /// check. + pub fn validate_scope_boundaries(&self, scope: &AuthzInfo) -> Result<(), AuthenticationError> { + match scope { + AuthzInfo::Domain(_domain) => { + if self.token_restriction.is_some() { + return Err(AuthenticationError::ScopeNotAllowed); + }; + match &self.authentication_context { + AuthenticationContext::ApplicationCredential(_) => { + Err(AuthenticationError::ScopeNotAllowed) + } + AuthenticationContext::Oidc(_) => Ok(()), + AuthenticationContext::K8s(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::Password => Ok(()), + AuthenticationContext::Token(_) => Ok(()), + AuthenticationContext::Trust(_trust) => { + Err(AuthenticationError::ScopeNotAllowed) + } + AuthenticationContext::WebauthN => Ok(()), + } + } + AuthzInfo::Project(project) => { + if let Some(token_restriction) = &self.token_restriction + && let Some(tr_pid) = &token_restriction.project_id + && *tr_pid != project.id + { + return Err(AuthenticationError::ScopeNotAllowed); + } + match &self.authentication_context { + AuthenticationContext::ApplicationCredential(app_cred) => { + if app_cred.project_id != project.id { + Err(AuthenticationError::ScopeNotAllowed) + } else { + Ok(()) + } + } + AuthenticationContext::Oidc(_) => Ok(()), + AuthenticationContext::K8s(_) => Ok(()), + AuthenticationContext::Password => Ok(()), + AuthenticationContext::Token(_) => Ok(()), + AuthenticationContext::Trust(trust) => { + if trust.project_id.as_ref().is_none_or(|x| *x != project.id) { + return Err(AuthenticationError::ScopeNotAllowed); + } + Ok(()) + } + AuthenticationContext::WebauthN => Ok(()), + } + } + AuthzInfo::Trust(_trust) => { + if self.token_restriction.is_some() { + return Err(AuthenticationError::ScopeNotAllowed); + }; + match &self.authentication_context { + AuthenticationContext::ApplicationCredential(_) => { + Err(AuthenticationError::ScopeNotAllowed) + } + AuthenticationContext::Oidc(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::K8s(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::Password => Ok(()), + AuthenticationContext::Token(_) => Ok(()), + AuthenticationContext::Trust(_trust) => { + Err(AuthenticationError::ScopeNotAllowed) + } + AuthenticationContext::WebauthN => Err(AuthenticationError::ScopeNotAllowed), + } + } + AuthzInfo::System(_system) => { + if self.token_restriction.is_some() { + return Err(AuthenticationError::ScopeNotAllowed); + }; + match &self.authentication_context { + // TODO: SPIFFE auth should be included here + AuthenticationContext::ApplicationCredential(_) => { + Err(AuthenticationError::ScopeNotAllowed) + } + AuthenticationContext::Oidc(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::K8s(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::Password => Ok(()), + AuthenticationContext::Token(_) => Ok(()), + AuthenticationContext::Trust(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::WebauthN => Ok(()), + } + } + AuthzInfo::Unscoped => { + if self.token_restriction.is_some() { + return Err(AuthenticationError::ScopeNotAllowed); + }; + match &self.authentication_context { + AuthenticationContext::ApplicationCredential(_) => { + Err(AuthenticationError::ScopeNotAllowed) + } + AuthenticationContext::Oidc(_) => Ok(()), + AuthenticationContext::K8s(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::Password => Ok(()), + AuthenticationContext::Token(_) => Ok(()), + AuthenticationContext::Trust(_) => Err(AuthenticationError::ScopeNotAllowed), + AuthenticationContext::WebauthN => Ok(()), + } + } + } + } +} + +impl TryFrom for SecurityContext { + type Error = AuthenticationError; + /// Construct a single-method [`SecurityContext`] from a single + /// [`AuthenticationResult`]. + /// + /// Generates a fresh audit ID, propagates any token audit IDs from the + /// parent token (when authenticated by token), and maps the + /// authentication result's context and principal into the security + /// context. + fn try_from(value: AuthenticationResult) -> Result { + let mut builder = SecurityContextBuilder::default(); + builder + .authentication_context(value.context.clone()) + .principal(value.principal.clone()); + + let mut audit_ids = vec![URL_SAFE_NO_PAD.encode(Uuid::new_v4().as_bytes())]; + if let AuthenticationContext::Token(token) = &value.context { + audit_ids.extend(token.audit_ids.clone()); + } + builder.audit_ids(audit_ids); + if let Some(token_restriction) = value.token_restriction { + builder.token_restriction(token_restriction); + } + builder.auth_methods(value.context.methods()); + Ok(builder.build()?) + } +} + +impl TryFrom> for SecurityContext { + type Error = AuthenticationError; + /// Construct a [`SecurityContext`] from multiple [`AuthenticationResult`]'s + /// (e.g., MFA). + /// + /// The first result provides the principal and primary authentication + /// context. All subsequent results must share the same principal; + /// otherwise [`AuthenticationError::AuthPrincipalDiffers`] is returned. + /// Audit IDs and authentication methods are aggregated across all results. + fn try_from(value: Vec) -> Result { + let mut builder = SecurityContextBuilder::default(); + let mut audit_ids = vec![URL_SAFE_NO_PAD.encode(Uuid::new_v4().as_bytes())]; + let mut auth_results = value.into_iter(); + + if let Some(auth) = auth_results.next() { + builder.principal(auth.principal.clone()); + builder.authentication_context(auth.context.clone()); + // TODO: process properly the token restrictions + if let Some(token_restriction) = auth.token_restriction { + builder.token_restriction(token_restriction); + } + if let AuthenticationContext::Token(token) = &auth.context { + audit_ids.extend(token.audit_ids.clone()); + }; + builder.audit_ids(audit_ids); + builder.auth_methods(auth.context.methods()); + } + let mut ctx = builder.build()?; + for auth in auth_results { + if auth.principal != ctx.principal { + return Err(AuthenticationError::AuthPrincipalDiffers); + } + if let AuthenticationContext::Token(token) = &auth.context { + ctx.audit_ids.extend(token.audit_ids.clone()); + }; + ctx.auth_methods.extend(auth.context.methods()); + } + + Ok(ctx) + } +} + +/// Principal information. +/// +/// Represent an entity that is trying to perform an action. +#[derive(Builder, Clone, Debug, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct PrincipalInfo { + /// Domain ID of the principal. + /// + /// The domain the principal belongs to. For the classical user it + /// represents the user domain_id. For the service accounts and other + /// remote principals it may be empty (e.g., internal service accounts + /// like nova, neutron, etc). + pub domain_id: Option, + + /// Principal identity. + pub identity: IdentityInfo, +} + +impl PrincipalInfo { + /// Validate the principal information + pub fn validate(&self) -> Result<(), AuthenticationError> { + self.identity.validate()?; + Ok(()) + } + /// Get the traditional user_id. + /// + /// For the regular user principal it is just the user_id. For the service + /// accounts, spiffe and k8s service accounts it is a virtual ID. + pub fn get_user_id(&self) -> String { + match &self.identity { + IdentityInfo::User(user) => user.user_id.clone(), + // Virtual ID for the Principal not existing as a regular user. + IdentityInfo::Principal(principal) => { + Uuid::new_v5(&NAMESPACE_UUID, principal.id.as_bytes()) + .simple() + .to_string() + } + } + } +} +/// Principal identity information. +#[derive(Clone, Debug, PartialEq)] +pub enum IdentityInfo { + /// Traditional user. + User(UserIdentityInfo), + /// A remote identity (Spiffe, SA, etc). + Principal(PrincipalIdentityInfo), +} + +impl IdentityInfo { + /// Validate the identity information: + /// + /// Dispatches to the appropriate variant's validation logic. + pub fn validate(&self) -> Result<(), AuthenticationError> { + match &self { + Self::User(user) => Ok(user.validate()?), + Self::Principal(principal) => Ok(principal.validate()?), + } + } +} + +/// Traditional Keystone User. +#[derive(Builder, Clone, Debug, PartialEq, Validate)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct UserIdentityInfo { /// Resolved user object. #[builder(default)] pub user: Option, @@ -116,19 +406,16 @@ pub struct AuthenticatedInfo { #[builder(default)] pub user_domain: Option, - /// Resolved user object. + /// Resolved user groups object. #[builder(default)] pub user_groups: Vec, /// User id. + #[validate(length(min = 1, max = 64))] pub user_id: String, } -impl AuthenticatedInfo { - pub fn builder() -> AuthenticatedInfoBuilder { - AuthenticatedInfoBuilder::default() - } - +impl UserIdentityInfo { /// Validate the authentication information: /// /// - User attribute must be set @@ -161,6 +448,124 @@ impl AuthenticatedInfo { } } +/// Workload principal. +#[derive(Builder, Clone, Debug, PartialEq, Validate)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct PrincipalIdentityInfo { + /// The unique identifier for the workload (e.g., SPIFFE ID or GitHub + /// Subject). + pub id: String, + + /// Metadata about the workload environment. + /// This allows OPA/Keystone to verify specific attributes like + /// 'repository'. + #[builder(default)] + pub attributes: HashMap, + + /// The source of the identity (e.g., "https://token.actions.githubusercontent.com"). + pub issuer: String, +} + +/// Authentication context. +#[derive(Clone, Debug, PartialEq)] +pub enum AuthenticationContext { + /// Login using application credentials. + ApplicationCredential(ApplicationCredential), + /// Login using OIDC federation + Oidc(OidcContext), + /// K8s Auth + K8s(K8sContext), + /// Login with password. + Password, + /// Login using regular fernet/jwt token. + Token(TokenContext), + /// Login consuming the trust. + Trust(Trust), + /// Login with WebauthN credentials. + WebauthN, +} + +/// K8s auth context. +#[derive(Builder, Clone, Debug, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct K8sContext { + /// Token restriction bound to the K8s auth role. + pub token_restriction_id: String, +} + +impl AuthenticationContext { + /// Get the authentication method names related to the authentication + /// context. + /// + /// When authenticated using the token this is technically just the list of + /// all methods already present in the token. For everything else it is + /// a new list of only the method itself. + pub fn methods(&self) -> HashSet { + match self { + Self::ApplicationCredential(_) => once("application_credential".to_string()).collect(), + Self::Oidc(_) => once("openid".to_string()).collect(), + Self::Password => once("password".to_string()).collect(), + Self::K8s(_) => once("mapped".to_string()).collect(), + Self::Token(token) => token + .methods + .iter() + .cloned() + .chain(once("token".to_string())) + .collect(), + Self::Trust(_) => once("trust".to_string()).collect(), + Self::WebauthN => once("x509".to_string()).collect(), + } + } +} + +/// OIDC auth context. +#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct OidcContext { + /// Federated IDP id. + pub idp_id: String, + + /// Federated protocol id. + pub protocol_id: String, +} + +/// Token auth context. +#[derive(Builder, Clone, Debug, Default, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into))] +pub struct TokenContext { + /// Audit IDs. + #[builder(default)] + pub audit_ids: Vec, + + /// Authentication expiration. + #[builder(default)] + pub expires_at: DateTime, + + /// Authentication methods. + #[builder(default)] + pub methods: Vec, +} + +/// Result of the single method Authentication +#[derive(Builder, Clone, Debug, PartialEq)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into, strip_option))] +pub struct AuthenticationResult { + /// The specific context for THIS factor (e.g., method name, audit IDs). + pub context: AuthenticationContext, + + /// The identity this provider identified/verified. + pub principal: PrincipalInfo, + + /// Token restriction rules tied to the authentication. + #[builder(default)] + pub token_restriction: Option, +} + /// Authorization information. #[derive(Clone, Debug, PartialEq)] pub enum AuthzInfo { @@ -169,7 +574,7 @@ pub enum AuthzInfo { /// Project scope. Project(Project), /// System scope. - System, + System(String), /// Trust scope. Trust(Trust), /// Unscoped. @@ -194,7 +599,7 @@ impl AuthzInfo { return Err(AuthenticationError::ProjectDisabled(project.id.clone())); } } - AuthzInfo::System => {} + AuthzInfo::System(_) => {} AuthzInfo::Trust(_) => {} AuthzInfo::Unscoped => {} } @@ -207,11 +612,17 @@ mod tests { use super::*; use std::collections::HashMap; + use crate::application_credential::ApplicationCredentialBuilder; use crate::identity::{UserOptions, UserResponse}; + use crate::token::TokenRestrictionBuilder; + use crate::trust::*; #[test] fn test_authn_validate_no_user() { - let authn = AuthenticatedInfo::builder().user_id("uid").build().unwrap(); + let authn = UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(); if let Err(AuthenticationError::Unauthorized) = authn.validate() { } else { panic!("should be unauthorized"); @@ -220,7 +631,7 @@ mod tests { #[test] fn test_authn_validate_user_disabled() { - let authn = AuthenticatedInfo::builder() + let authn = UserIdentityInfoBuilder::default() .user_id("uid") .user(UserResponse { id: "uid".to_string(), @@ -244,7 +655,7 @@ mod tests { #[test] fn test_authn_validate_user_mismatch() { - let authn = AuthenticatedInfo::builder() + let authn = UserIdentityInfoBuilder::default() .user_id("uid1") .user(UserResponse { id: "uid2".to_string(), @@ -317,7 +728,7 @@ mod tests { #[test] fn test_authz_validate_system() { - let authz = AuthzInfo::System; + let authz = AuthzInfo::System("system".into()); assert!(authz.validate().is_ok()); } @@ -326,4 +737,487 @@ mod tests { let authz = AuthzInfo::Unscoped; assert!(authz.validate().is_ok()); } + + #[test] + fn test_validate_scope_boundarires_with_token_restriction() { + let project = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let project2 = AuthzInfo::Project(Project { + id: "pid2".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let domain = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + let trust = AuthzInfo::Trust( + TrustBuilder::default() + .id("trust_id") + .trustor_user_id("trustor") + .trustee_user_id("trustee") + .impersonation(false) + .build() + .unwrap(), + ); + let system = AuthzInfo::System("system".into()); + let unscoped = AuthzInfo::Unscoped; + let tr = TokenRestrictionBuilder::default() + .allow_rescope(true) + .allow_renew(true) + .id("tr_id") + .domain_id("did") + .role_ids(vec![]) + .project_id("pid") + .build() + .unwrap(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .token_restriction(tr.clone()) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + assert!(matches!( + ctx.validate_scope_boundaries(&domain), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(ctx.validate_scope_boundaries(&project).is_ok()); + assert!( + matches!( + ctx.validate_scope_boundaries(&project2), + Err(AuthenticationError::ScopeNotAllowed), + ), + "TR restricted to the other project" + ); + assert!(matches!( + ctx.validate_scope_boundaries(&trust), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&system), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&unscoped), + Err(AuthenticationError::ScopeNotAllowed) + )); + } + + #[test] + fn test_validate_scope_boundaries_app_cred() { + let project = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let project2 = AuthzInfo::Project(Project { + id: "pid2".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let domain = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + let trust = AuthzInfo::Trust( + TrustBuilder::default() + .id("trust_id") + .trustor_user_id("trustor") + .trustee_user_id("trustee") + .impersonation(false) + .build() + .unwrap(), + ); + let system = AuthzInfo::System("system".into()); + let unscoped = AuthzInfo::Unscoped; + let app_cred = ApplicationCredentialBuilder::default() + .id("app_cred_id") + .name("app_cred_name") + .project_id("pid") + .roles(vec![]) + .unrestricted(false) + .user_id("uid") + .build() + .unwrap(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::ApplicationCredential(app_cred)) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + assert!(matches!( + ctx.validate_scope_boundaries(&domain), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(ctx.validate_scope_boundaries(&project).is_ok()); + assert!(matches!( + ctx.validate_scope_boundaries(&project2), + Err(AuthenticationError::ScopeNotAllowed), + ),); + assert!(matches!( + ctx.validate_scope_boundaries(&trust), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&system), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&unscoped), + Err(AuthenticationError::ScopeNotAllowed) + )); + } + + #[test] + fn test_validate_scope_boundaries_oidc() { + let project = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let project2 = AuthzInfo::Project(Project { + id: "pid2".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let domain = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + let trust = AuthzInfo::Trust( + TrustBuilder::default() + .id("trust_id") + .trustor_user_id("trustor") + .trustee_user_id("trustee") + .impersonation(false) + .build() + .unwrap(), + ); + let system = AuthzInfo::System("system".into()); + let unscoped = AuthzInfo::Unscoped; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Oidc( + OidcContextBuilder::default() + .idp_id("idp") + .protocol_id("protocol") + .build() + .unwrap(), + )) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + assert!(ctx.validate_scope_boundaries(&domain).is_ok()); + assert!(ctx.validate_scope_boundaries(&project).is_ok()); + assert!(ctx.validate_scope_boundaries(&project2).is_ok()); + assert!(matches!( + ctx.validate_scope_boundaries(&trust), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&system), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(ctx.validate_scope_boundaries(&unscoped).is_ok()); + } + + #[test] + fn test_validate_scope_boundarires_k8s() { + let project = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let project2 = AuthzInfo::Project(Project { + id: "pid2".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let domain = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + let trust = AuthzInfo::Trust( + TrustBuilder::default() + .id("trust_id") + .trustor_user_id("trustor") + .trustee_user_id("trustee") + .impersonation(false) + .build() + .unwrap(), + ); + let system = AuthzInfo::System("system".into()); + let unscoped = AuthzInfo::Unscoped; + let tr = TokenRestrictionBuilder::default() + .allow_rescope(true) + .allow_renew(true) + .id("tr_id") + .domain_id("did") + .role_ids(vec![]) + .project_id("pid") + .build() + .unwrap(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::K8s( + K8sContextBuilder::default() + .token_restriction_id(tr.id.clone()) + .build() + .unwrap(), + )) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .token_restriction(tr.clone()) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + assert!(matches!( + ctx.validate_scope_boundaries(&domain), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(ctx.validate_scope_boundaries(&project).is_ok()); + assert!(matches!( + ctx.validate_scope_boundaries(&project2), + Err(AuthenticationError::ScopeNotAllowed), + ),); + assert!(matches!( + ctx.validate_scope_boundaries(&trust), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&system), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&unscoped), + Err(AuthenticationError::ScopeNotAllowed) + )); + } + + #[test] + fn test_validate_scope_boundaries_password() { + let project = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let project2 = AuthzInfo::Project(Project { + id: "pid2".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let domain = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + let trust = AuthzInfo::Trust( + TrustBuilder::default() + .id("trust_id") + .trustor_user_id("trustor") + .trustee_user_id("trustee") + .impersonation(false) + .build() + .unwrap(), + ); + let system = AuthzInfo::System("system".into()); + let unscoped = AuthzInfo::Unscoped; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + assert!(ctx.validate_scope_boundaries(&domain).is_ok()); + assert!(ctx.validate_scope_boundaries(&project).is_ok()); + assert!(ctx.validate_scope_boundaries(&project2).is_ok()); + assert!(ctx.validate_scope_boundaries(&trust).is_ok()); + assert!(ctx.validate_scope_boundaries(&system).is_ok()); + assert!(ctx.validate_scope_boundaries(&unscoped).is_ok()); + } + + #[test] + fn test_validate_scope_boundarires_trust() { + let project = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let project2 = AuthzInfo::Project(Project { + id: "pid2".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let domain = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + let trust = TrustBuilder::default() + .id("trust_id") + .trustor_user_id("trustor") + .trustee_user_id("trustee") + .project_id("pid") + .impersonation(false) + .build() + .unwrap(); + let trust_scope = AuthzInfo::Trust(trust.clone()); + let system = AuthzInfo::System("system".into()); + let unscoped = AuthzInfo::Unscoped; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Trust(trust.clone())) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + assert!(matches!( + ctx.validate_scope_boundaries(&domain), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(ctx.validate_scope_boundaries(&project).is_ok()); + assert!(matches!( + ctx.validate_scope_boundaries(&project2), + Err(AuthenticationError::ScopeNotAllowed), + ),); + assert!(matches!( + ctx.validate_scope_boundaries(&trust_scope), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&system), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(matches!( + ctx.validate_scope_boundaries(&unscoped), + Err(AuthenticationError::ScopeNotAllowed) + )); + } + + #[test] + fn test_validate_scope_boundaries_webauthn() { + let project = AuthzInfo::Project(Project { + id: "pid".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let project2 = AuthzInfo::Project(Project { + id: "pid2".into(), + domain_id: "pdid".into(), + enabled: true, + ..Default::default() + }); + let domain = AuthzInfo::Domain(Domain { + id: "id".into(), + name: "name".into(), + enabled: true, + ..Default::default() + }); + let trust = AuthzInfo::Trust( + TrustBuilder::default() + .id("trust_id") + .trustor_user_id("trustor") + .trustee_user_id("trustee") + .impersonation(false) + .build() + .unwrap(), + ); + let system = AuthzInfo::System("system".into()); + let unscoped = AuthzInfo::Unscoped; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::WebauthN) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + assert!(ctx.validate_scope_boundaries(&domain).is_ok()); + assert!(ctx.validate_scope_boundaries(&project).is_ok()); + assert!(ctx.validate_scope_boundaries(&project2).is_ok()); + assert!(matches!( + ctx.validate_scope_boundaries(&trust), + Err(AuthenticationError::ScopeNotAllowed) + )); + assert!(ctx.validate_scope_boundaries(&system).is_ok()); + assert!(ctx.validate_scope_boundaries(&unscoped).is_ok()); + } } diff --git a/crates/core-types/src/k8s_auth/error.rs b/crates/core-types/src/k8s_auth/error.rs index 3a7b6062..5805b670 100644 --- a/crates/core-types/src/k8s_auth/error.rs +++ b/crates/core-types/src/k8s_auth/error.rs @@ -145,6 +145,10 @@ pub enum K8sAuthProviderError { #[error("token restriction {0} not found")] TokenRestrictionNotFound(String), + /// Token restriction not found. + #[error("token restriction missing in the authentication result")] + TokenRestrictionMissing, + /// Token restriction MUST specify the `project_id`. #[error("token restriction must specify `project_id`")] TokenRestrictionMustSpecifyProjectId, diff --git a/crates/core-types/src/lib.rs b/crates/core-types/src/lib.rs index c26a54ab..46ce2265 100644 --- a/crates/core-types/src/lib.rs +++ b/crates/core-types/src/lib.rs @@ -29,6 +29,7 @@ pub mod resource; pub mod revoke; pub mod role; pub mod scope; +//pub mod security_context; pub mod token; pub mod trust; diff --git a/crates/core-types/src/revoke/revocation_event.rs b/crates/core-types/src/revoke/revocation_event.rs index 2b2cfc4d..bb70195d 100644 --- a/crates/core-types/src/revoke/revocation_event.rs +++ b/crates/core-types/src/revoke/revocation_event.rs @@ -168,7 +168,7 @@ impl TryFrom<&Token> for RevocationEventListParameters { .user() .iter() .map(|user| user.domain_id.clone()) - .chain(value.domain().map(|domain| domain.id.clone())) + .chain(value.domain_id().cloned()) .collect::>(), ), expires_at: None, diff --git a/crates/core-types/src/token.rs b/crates/core-types/src/token.rs index c7756410..f87a6100 100644 --- a/crates/core-types/src/token.rs +++ b/crates/core-types/src/token.rs @@ -16,6 +16,7 @@ use chrono::{DateTime, Utc}; use serde::Serialize; use validator::Validate; +use crate::auth::*; use crate::identity::UserResponse; use crate::resource::{Domain, Project}; use crate::role::RoleRef; @@ -54,7 +55,80 @@ pub enum Token { Unscoped(UnscopedPayload), } +// TODO: From for SecurityContext + impl Token { + /// Construct the [`Token`] for the requested [`AuthzInfo`] with the current + /// [`SecurityContext`].` + /// + /// # Security Note + /// This is the low-level method with no real validation whether the token + /// can be issued. + pub fn from_security_context_with_scope( + ctx: &SecurityContext, + scope: &AuthzInfo, + expires_at: DateTime, + ) -> Result { + if let Some(token_restriction) = &ctx.token_restriction { + return Ok(Self::Restricted(RestrictedPayload::from_security_context( + ctx, + token_restriction, + expires_at, + )?)); + } + match scope { + AuthzInfo::Domain(domain) => match &ctx.authentication_context { + AuthenticationContext::Oidc(oidc) => Ok(Self::FederationDomainScope( + FederationDomainScopePayload::from_security_context( + ctx, domain, oidc, expires_at, + )?, + )), + AuthenticationContext::Trust(_trust) => todo!(), + _ => Ok(Self::DomainScope( + DomainScopePayload::from_security_context(ctx, domain, expires_at)?, + )), + }, + AuthzInfo::Project(project) => match &ctx.authentication_context { + AuthenticationContext::ApplicationCredential(app_cred) => { + Ok(Self::ApplicationCredential( + ApplicationCredentialPayload::from_security_context( + ctx, app_cred, expires_at, + )?, + )) + } + AuthenticationContext::Oidc(oidc) => Ok(Self::FederationProjectScope( + FederationProjectScopePayload::from_security_context( + ctx, project, oidc, expires_at, + )?, + )), + _ => Ok(Self::ProjectScope( + ProjectScopePayload::from_security_context(ctx, project, expires_at)?, + )), + }, + AuthzInfo::Trust(trust) => Ok(match &trust.project_id { + Some(project_id) => Self::Trust(TrustPayload::from_security_context( + ctx, + trust, + project_id.clone(), + expires_at, + )?), + None => todo!(), + }), + AuthzInfo::System(system) => Ok(Self::SystemScope( + SystemScopePayload::from_security_context(ctx, system, expires_at)?, + )), + AuthzInfo::Unscoped => match &ctx.authentication_context { + AuthenticationContext::Oidc(oidc) => Ok(Self::FederationUnscoped( + FederationUnscopedPayload::from_security_context(ctx, oidc, expires_at)?, + )), + + _ => Ok(Self::Unscoped(UnscopedPayload::from_security_context( + ctx, expires_at, + )?)), + }, + } + } + pub const fn user_id(&self) -> &String { match self { Self::ApplicationCredential(x) => &x.user_id, @@ -192,6 +266,22 @@ impl Token { } } + /// Get the domain ID for domain-scoped tokens. + /// + /// Returns the domain identifier when the token is a [`Token::DomainScope`] + /// or [`Token::FederationDomainScope`]. Returns `None` for all other + /// token types. + pub const fn domain_id(&self) -> Option<&String> { + match self { + Self::DomainScope(x) => Some(&x.domain_id), + Self::FederationDomainScope(x) => Some(&x.domain_id), + _ => None, + } + } + + /// Get the resolved [`Domain`] if present in the token. It may be empty for + /// the DomainScope and FederationDomainScope token when it was not + /// previously minted. pub const fn domain(&self) -> Option<&Domain> { match self { Self::DomainScope(x) => x.domain.as_ref(), diff --git a/crates/core-types/src/token/error.rs b/crates/core-types/src/token/error.rs index e833537e..1cc9a38a 100644 --- a/crates/core-types/src/token/error.rs +++ b/crates/core-types/src/token/error.rs @@ -152,6 +152,10 @@ pub enum TokenProviderError { #[error("driver `{0}` is not supported for the token provider")] UnsupportedDriver(String), + /// Unsupported identity (e.g., virtual principle). + #[error("unsupported principle type")] + UnsupportedPrinciple, + /// Unsupported token restriction driver. #[error("driver `{0}` is not supported for the token restriction provider")] UnsupportedTRDriver(String), diff --git a/crates/core-types/src/token/payload/application_credential.rs b/crates/core-types/src/token/payload/application_credential.rs index e87f24fb..bdfb943d 100644 --- a/crates/core-types/src/token/payload/application_credential.rs +++ b/crates/core-types/src/token/payload/application_credential.rs @@ -19,11 +19,13 @@ use validator::Validate; use super::common; use crate::application_credential::ApplicationCredential; +use crate::auth::SecurityContext; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::resource::Project; use crate::role::RoleRef; use crate::token::Token; +use crate::token::error::TokenProviderError; #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] @@ -90,3 +92,78 @@ impl From for Token { Self::ApplicationCredential(value) } } + +impl ApplicationCredentialPayload { + /// Construct an application credential token payload from a + /// [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, and audit + /// IDs from the context. Expiration is the minimum of the credential's + /// own expiration and the provided `expires_at`. + pub fn from_security_context( + ctx: &SecurityContext, + app_cred: &ApplicationCredential, + expires_at: DateTime, + ) -> Result { + Ok(ApplicationCredentialPayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at( + app_cred + .expires_at + .map(|ac_expiry| std::cmp::min(expires_at, ac_expiry)) + .unwrap_or(expires_at), + ) + .application_credential_id(app_cred.id.clone()) + .application_credential(app_cred.clone()) + .project_id(app_cred.project_id.clone()) + .build()?) + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::application_credential::*; + use crate::auth::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let app_cred = ApplicationCredentialBuilder::default() + .id("app_cred_id") + .name("app_cred_name") + .project_id("pid") + .user_id("uid2") + .unrestricted(false) + .roles(vec![]) + .build() + .unwrap(); + + let payload = + ApplicationCredentialPayload::from_security_context(&ctx, &app_cred, now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id, "ensure uid of Context is taken"); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("pid", payload.project_id); + assert_eq!("app_cred_id", payload.application_credential_id); + } +} diff --git a/crates/core-types/src/token/payload/domain_scoped.rs b/crates/core-types/src/token/payload/domain_scoped.rs index 4c39b8eb..a04b5cab 100644 --- a/crates/core-types/src/token/payload/domain_scoped.rs +++ b/crates/core-types/src/token/payload/domain_scoped.rs @@ -18,11 +18,13 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::SecurityContext; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::resource::Domain; use crate::role::RoleRef; use crate::token::Token; +use crate::token::error::TokenProviderError; #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] @@ -83,3 +85,65 @@ impl From for Token { Self::DomainScope(value) } } + +impl DomainScopePayload { + /// Construct a domain-scoped token payload from a [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, and audit + /// IDs from the context. + pub fn from_security_context( + ctx: &SecurityContext, + domain: &Domain, + expires_at: DateTime, + ) -> Result { + Ok(DomainScopePayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .domain_id(domain.id.clone()) + .domain(domain.clone()) + .build()?) + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + use crate::resource::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let domain = DomainBuilder::default() + .id("did") + .name("pname") + .enabled(true) + .build() + .unwrap(); + + let payload = DomainScopePayload::from_security_context(&ctx, &domain, now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("did", payload.domain_id); + } +} diff --git a/crates/core-types/src/token/payload/federation_domain_scoped.rs b/crates/core-types/src/token/payload/federation_domain_scoped.rs index 68e39bb5..59a76500 100644 --- a/crates/core-types/src/token/payload/federation_domain_scoped.rs +++ b/crates/core-types/src/token/payload/federation_domain_scoped.rs @@ -18,11 +18,13 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::{IdentityInfo, OidcContext, SecurityContext}; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::resource::Domain; use crate::role::RoleRef; use crate::token::Token; +use crate::token::error::TokenProviderError; /// Federated domain scope token payload. #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] @@ -49,11 +51,14 @@ pub struct FederationDomainScopePayload { #[validate(length(min = 1, max = 64))] pub protocol_id: String, + + #[builder(setter(name = _group_ids))] pub group_ids: Vec, #[builder(default)] pub issued_at: DateTime, + // Fields not serialized into the token. #[builder(default)] pub user: Option, #[builder(default)] @@ -84,6 +89,23 @@ impl FederationDomainScopePayloadBuilder { .extend(iter.map(Into::into)); self } + + /// Set the group IDs for the federated domain-scoped payload. + /// + /// Collects group identifiers from an iterator, allowing the builder to + /// accept any iterable of values that can be converted into `String`. + /// The previous auto-generated setter is intentionally hidden in favor + /// of this iterator-based API. + pub fn group_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.group_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } } impl From for Token { @@ -91,3 +113,85 @@ impl From for Token { Self::FederationDomainScope(value) } } + +impl FederationDomainScopePayload { + /// Construct a federated domain-scoped token payload from a + /// [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, audit IDs, + /// and user group IDs from the context, and fills in OIDC-specific + /// fields (IDP ID and protocol ID) from the provided [`OidcContext`]. + /// Returns [`TokenProviderError::UnsupportedPrinciple`] when the + /// principal is not a traditional user. + pub fn from_security_context( + ctx: &SecurityContext, + domain: &Domain, + oidc: &OidcContext, + expires_at: DateTime, + ) -> Result { + match &ctx.principal.identity { + IdentityInfo::User(user) => Ok(FederationDomainScopePayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .domain_id(domain.id.clone()) + .domain(domain.clone()) + .idp_id(oidc.idp_id.clone()) + .protocol_id(oidc.protocol_id.clone()) + .group_ids(user.user_groups.iter().map(|g| g.id.clone())) + .build()?), + _ => Err(TokenProviderError::UnsupportedPrinciple), + } + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + use crate::resource::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let oidc = OidcContextBuilder::default() + .idp_id("idp") + .protocol_id("protocol_id") + .build() + .unwrap(); + + let domain = DomainBuilder::default() + .id("did") + .name("pname") + .enabled(true) + .build() + .unwrap(); + + let payload = + FederationDomainScopePayload::from_security_context(&ctx, &domain, &oidc, now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("idp", payload.idp_id); + assert_eq!("protocol_id", payload.protocol_id); + assert_eq!("did", payload.domain_id); + } +} diff --git a/crates/core-types/src/token/payload/federation_project_scoped.rs b/crates/core-types/src/token/payload/federation_project_scoped.rs index 4dcd8a1a..01fc57e1 100644 --- a/crates/core-types/src/token/payload/federation_project_scoped.rs +++ b/crates/core-types/src/token/payload/federation_project_scoped.rs @@ -18,11 +18,13 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::{IdentityInfo, OidcContext, SecurityContext}; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::resource::Project; use crate::role::RoleRef; use crate::token::Token; +use crate::token::error::TokenProviderError; /// Federated project scope token payload. #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] @@ -48,6 +50,7 @@ pub struct FederationProjectScopePayload { pub idp_id: String, #[validate(length(min = 1, max = 64))] pub protocol_id: String, + #[builder(setter(name = _group_ids))] pub group_ids: Vec, #[builder(default)] @@ -82,6 +85,21 @@ impl FederationProjectScopePayloadBuilder { .extend(iter.map(Into::into)); self } + + /// Set the group IDs for the federated project-scoped payload. + /// + /// Collects group identifiers from an iterator, allowing the builder to + /// accept any iterable of values that can be converted into `String`. + pub fn group_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.group_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } } impl From for Token { @@ -89,3 +107,87 @@ impl From for Token { Self::FederationProjectScope(value) } } + +impl FederationProjectScopePayload { + /// Construct a federated project-scoped token payload from a + /// [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, audit IDs, + /// and user group IDs from the context, and fills in OIDC-specific + /// fields (IDP ID and protocol ID) from the provided [`OidcContext`]. + /// Returns [`TokenProviderError::UnsupportedPrinciple`] when the + /// principal is not a traditional user. + pub fn from_security_context( + ctx: &SecurityContext, + project: &Project, + oidc: &OidcContext, + expires_at: DateTime, + ) -> Result { + match &ctx.principal.identity { + IdentityInfo::User(user) => Ok(FederationProjectScopePayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .project_id(project.id.clone()) + .project(project.clone()) + .idp_id(oidc.idp_id.clone()) + .protocol_id(oidc.protocol_id.clone()) + .group_ids(user.user_groups.iter().map(|g| g.id.clone()).into_iter()) + .build()?), + _ => Err(TokenProviderError::UnsupportedPrinciple), + } + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + use crate::resource::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let oidc = OidcContextBuilder::default() + .idp_id("idp") + .protocol_id("protocol_id") + .build() + .unwrap(); + + let project = ProjectBuilder::default() + .id("pid") + .domain_id("did") + .name("pname") + .enabled(true) + .build() + .unwrap(); + + let payload = + FederationProjectScopePayload::from_security_context(&ctx, &project, &oidc, now) + .unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("idp", payload.idp_id); + assert_eq!("protocol_id", payload.protocol_id); + assert_eq!("pid", payload.project_id); + } +} diff --git a/crates/core-types/src/token/payload/federation_unscoped.rs b/crates/core-types/src/token/payload/federation_unscoped.rs index 841fad2f..d52f5670 100644 --- a/crates/core-types/src/token/payload/federation_unscoped.rs +++ b/crates/core-types/src/token/payload/federation_unscoped.rs @@ -18,9 +18,11 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::{IdentityInfo, OidcContext, SecurityContext}; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::token::Token; +use crate::token::error::TokenProviderError; /// Federated unscoped token payload. #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] @@ -44,6 +46,7 @@ pub struct FederationUnscopedPayload { #[validate(length(min = 1, max = 64))] pub protocol_id: String, + #[builder(setter(name = _group_ids))] pub group_ids: Vec, #[builder(default)] @@ -74,6 +77,21 @@ impl FederationUnscopedPayloadBuilder { .extend(iter.map(Into::into)); self } + + /// Set the group IDs for the federated unscoped payload. + /// + /// Collects group identifiers from an iterator, allowing the builder to + /// accept any iterable of values that can be converted into `String`. + pub fn group_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.group_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } } impl From for Token { @@ -81,3 +99,70 @@ impl From for Token { Self::FederationUnscoped(value) } } + +impl FederationUnscopedPayload { + /// Construct a federated unscoped token payload from a [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, audit IDs, + /// and user group IDs from the context, and fills in OIDC-specific + /// fields (IDP ID and protocol ID) from the provided [`OidcContext`]. + /// Returns [`TokenProviderError::UnsupportedPrinciple`] when the + /// principal is not a traditional user. + pub fn from_security_context( + ctx: &SecurityContext, + oidc: &OidcContext, + expires_at: DateTime, + ) -> Result { + match &ctx.principal.identity { + IdentityInfo::User(user) => Ok(FederationUnscopedPayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .idp_id(oidc.idp_id.clone()) + .protocol_id(oidc.protocol_id.clone()) + .group_ids(user.user_groups.iter().map(|g| g.id.clone())) + .build()?), + _ => Err(TokenProviderError::UnsupportedPrinciple), + } + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let oidc = OidcContextBuilder::default() + .idp_id("idp") + .protocol_id("protocol_id") + .build() + .unwrap(); + let payload = FederationUnscopedPayload::from_security_context(&ctx, &oidc, now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("idp", payload.idp_id); + assert_eq!("protocol_id", payload.protocol_id); + } +} diff --git a/crates/core-types/src/token/payload/project_scoped.rs b/crates/core-types/src/token/payload/project_scoped.rs index 5b08a564..5c32a7e4 100644 --- a/crates/core-types/src/token/payload/project_scoped.rs +++ b/crates/core-types/src/token/payload/project_scoped.rs @@ -18,11 +18,13 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::SecurityContext; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::resource::Project; use crate::role::RoleRef; use crate::token::Token; +use crate::token::error::TokenProviderError; #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] @@ -83,3 +85,65 @@ impl From for Token { Self::ProjectScope(value) } } + +impl ProjectScopePayload { + /// Construct a project-scoped token payload from a [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, and audit + /// IDs from the context. + pub fn from_security_context( + ctx: &SecurityContext, + project: &Project, + expires_at: DateTime, + ) -> Result { + Ok(ProjectScopePayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .project_id(project.id.clone()) + .project(project.clone()) + .build()?) + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + use crate::resource::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let project = ProjectBuilder::default() + .id("pid") + .domain_id("did") + .name("pname") + .enabled(true) + .build() + .unwrap(); + let payload = ProjectScopePayload::from_security_context(&ctx, &project, now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("pid", payload.project_id); + } +} diff --git a/crates/core-types/src/token/payload/restricted.rs b/crates/core-types/src/token/payload/restricted.rs index 3616f3d8..68731dd5 100644 --- a/crates/core-types/src/token/payload/restricted.rs +++ b/crates/core-types/src/token/payload/restricted.rs @@ -19,11 +19,13 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::{AuthzInfo, IdentityInfo, SecurityContext}; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::resource::Project; use crate::role::RoleRef; -use crate::token::Token; +use crate::token::error::TokenProviderError; +use crate::token::{Token, TokenRestriction}; /// Restricted token payload. #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] @@ -94,3 +96,95 @@ impl From for Token { Self::Restricted(value) } } + +impl RestrictedPayload { + /// Construct a restricted token payload from a [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, and audit + /// IDs from the context. The user ID is taken from the restriction if + /// explicitly set, otherwise falls back to the context's principal. + /// Requires a project scope (either from the restriction or the context); + /// returns [`TokenProviderError::RestrictedTokenNotProjectScoped`] when no + /// project is available. + pub fn from_security_context( + ctx: &SecurityContext, + restriction: &TokenRestriction, + expires_at: DateTime, + ) -> Result { + match &ctx.principal.identity { + IdentityInfo::User(user) => Ok(RestrictedPayloadBuilder::default() + .user_id( + restriction + .user_id + .as_ref() + .unwrap_or(&user.user_id.clone()), + ) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .token_restriction_id(restriction.id.clone()) + .project_id( + restriction + .project_id + .as_ref() + .or(match &ctx.authorization { + Some(AuthzInfo::Project(project)) => Some(&project.id), + _ => None, + }) + .ok_or_else(|| TokenProviderError::RestrictedTokenNotProjectScoped)?, + ) + .allow_renew(restriction.allow_renew) + .allow_rescope(restriction.allow_rescope) + .roles(restriction.roles.clone()) + .build()?), + _ => { + todo!(); + } + } + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + use crate::token::restriction::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let tr = TokenRestrictionBuilder::default() + .id("rid") + .domain_id("did") + .project_id("pid") + .allow_renew(true) + .allow_rescope(true) + .role_ids([]) + .build() + .unwrap(); + let payload = RestrictedPayload::from_security_context(&ctx, &tr, now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("pid", payload.project_id); + assert!(payload.allow_renew); + assert!(payload.allow_rescope); + } +} diff --git a/crates/core-types/src/token/payload/system_scoped.rs b/crates/core-types/src/token/payload/system_scoped.rs index 07198eca..dbf51e1e 100644 --- a/crates/core-types/src/token/payload/system_scoped.rs +++ b/crates/core-types/src/token/payload/system_scoped.rs @@ -18,10 +18,12 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::SecurityContext; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::role::RoleRef; use crate::token::Token; +use crate::token::error::TokenProviderError; #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] @@ -80,3 +82,56 @@ impl From for Token { Self::SystemScope(value) } } + +impl SystemScopePayload { + /// Construct a system-scoped token payload from a [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, and audit + /// IDs from the context. + pub fn from_security_context>( + ctx: &SecurityContext, + system: S, + expires_at: DateTime, + ) -> Result { + Ok(SystemScopePayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .system_id(system) + .build()?) + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let payload = SystemScopePayload::from_security_context(&ctx, "system", now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("system", payload.system_id); + } +} diff --git a/crates/core-types/src/token/payload/trust.rs b/crates/core-types/src/token/payload/trust.rs index 8eebfc19..6f52bb05 100644 --- a/crates/core-types/src/token/payload/trust.rs +++ b/crates/core-types/src/token/payload/trust.rs @@ -19,10 +19,12 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::SecurityContext; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::resource::Project; use crate::token::Token; +use crate::token::error::TokenProviderError; use crate::trust::Trust; /// Trust token payload. @@ -94,3 +96,68 @@ impl From for Token { Self::Trust(value) } } + +impl TrustPayload { + /// Construct a trust-scoped token payload from a [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, and audit + /// IDs from the context, and embeds the trust object and its identifier + /// into the payload. + pub fn from_security_context>( + ctx: &SecurityContext, + trust: &Trust, + project_id: S, + expires_at: DateTime, + ) -> Result { + Ok(TrustPayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .trust_id(trust.id.clone()) + .trust(trust.clone()) + .project_id(project_id) + .build()?) + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::auth::*; + use crate::trust::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let trust = TrustBuilder::default() + .id("tid") + .impersonation(false) + .trustor_user_id("trustor_uid") + .trustee_user_id("trustor_uid") + .build() + .unwrap(); + let payload = TrustPayload::from_security_context(&ctx, &trust, "pid", now).unwrap(); + assert_eq!(now, payload.expires_at); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["password"], payload.methods); + assert_eq!("tid", payload.trust_id); + } +} diff --git a/crates/core-types/src/token/payload/unscoped.rs b/crates/core-types/src/token/payload/unscoped.rs index a405cf09..b1d3857f 100644 --- a/crates/core-types/src/token/payload/unscoped.rs +++ b/crates/core-types/src/token/payload/unscoped.rs @@ -18,9 +18,11 @@ use serde::Serialize; use validator::Validate; use super::common; +use crate::auth::SecurityContext; use crate::error::BuilderError; use crate::identity::UserResponse; use crate::token::Token; +use crate::token::error::TokenProviderError; #[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] @@ -74,3 +76,64 @@ impl From for Token { Self::Unscoped(value) } } + +impl UnscopedPayload { + /// Construct an unscoped token payload from a [`SecurityContext`]. + /// + /// Propagates the principal's user ID, authentication methods, and audit + /// IDs from the context. + pub fn from_security_context( + ctx: &SecurityContext, + expires_at: DateTime, + ) -> Result { + Ok(UnscopedPayloadBuilder::default() + .user_id(ctx.principal.get_user_id()) + .methods(ctx.auth_methods.iter()) + .audit_ids(ctx.audit_ids.iter()) + .expires_at(expires_at) + .build()?) + } +} + +#[cfg(test)] +mod tests { + use chrono::{TimeDelta, Utc}; + + use super::*; + use crate::auth::*; + + #[test] + fn test_create_from_security_context() { + let now = Utc::now(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Token( + TokenContextBuilder::default() + .expires_at(now.clone()) + .build() + .unwrap(), + )) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + let payload = UnscopedPayload::from_security_context( + &ctx, + now.checked_add_signed(TimeDelta::hours(1)).unwrap(), + ) + .unwrap(); + assert_eq!( + now.checked_add_signed(TimeDelta::hours(1)).unwrap(), + payload.expires_at + ); + assert_eq!("uid", payload.user_id); + assert_eq!(vec!["token"], payload.methods); + } +} diff --git a/crates/core/src/identity/backend.rs b/crates/core/src/identity/backend.rs index 93efed9c..182fb40e 100644 --- a/crates/core/src/identity/backend.rs +++ b/crates/core/src/identity/backend.rs @@ -18,7 +18,7 @@ use std::collections::HashSet; use openstack_keystone_core_types::identity::*; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::identity::IdentityProviderError; use crate::keystone::ServiceState; @@ -86,7 +86,7 @@ pub trait IdentityBackend: Send + Sync { &self, state: &ServiceState, auth: &UserPasswordAuthRequest, - ) -> Result; + ) -> Result; /// Create group. /// diff --git a/crates/core/src/identity/mock.rs b/crates/core/src/identity/mock.rs index f41ec235..27331700 100644 --- a/crates/core/src/identity/mock.rs +++ b/crates/core/src/identity/mock.rs @@ -19,7 +19,7 @@ use std::collections::HashSet; use openstack_keystone_core_types::identity::*; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::identity::{IdentityApi, error::IdentityProviderError}; use crate::keystone::ServiceState; @@ -60,7 +60,7 @@ mock! { &self, state: &ServiceState, auth: &UserPasswordAuthRequest, - ) -> Result; + ) -> Result; async fn create_group( &self, diff --git a/crates/core/src/identity/mod.rs b/crates/core/src/identity/mod.rs index c575940f..f3780e60 100644 --- a/crates/core/src/identity/mod.rs +++ b/crates/core/src/identity/mod.rs @@ -49,7 +49,7 @@ pub use mock::MockIdentityProvider; mod provider_api; mod service; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManagerApi; use service::IdentityService; @@ -186,7 +186,7 @@ impl IdentityApi for IdentityProvider { &self, state: &ServiceState, auth: &UserPasswordAuthRequest, - ) -> Result { + ) -> Result { match self { Self::Service(provider) => provider.authenticate_by_password(state, auth).await, #[cfg(any(test, feature = "mock"))] diff --git a/crates/core/src/identity/provider_api.rs b/crates/core/src/identity/provider_api.rs index 8f97a68d..64dd6646 100644 --- a/crates/core/src/identity/provider_api.rs +++ b/crates/core/src/identity/provider_api.rs @@ -18,7 +18,7 @@ use std::collections::HashSet; use openstack_keystone_core_types::identity::*; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::identity::IdentityProviderError; use crate::keystone::ServiceState; @@ -85,7 +85,7 @@ pub trait IdentityApi: Send + Sync { &self, state: &ServiceState, auth: &UserPasswordAuthRequest, - ) -> Result; + ) -> Result; /// Create a new group. /// diff --git a/crates/core/src/identity/service.rs b/crates/core/src/identity/service.rs index 2c63302e..5010fc92 100644 --- a/crates/core/src/identity/service.rs +++ b/crates/core/src/identity/service.rs @@ -25,7 +25,7 @@ use validator::Validate; use openstack_keystone_config::Config; use openstack_keystone_core_types::identity::*; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::identity::{IdentityApi, IdentityProviderError, backend::IdentityBackend}; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManagerApi; @@ -155,7 +155,7 @@ impl IdentityApi for IdentityService { &self, state: &ServiceState, auth: &UserPasswordAuthRequest, - ) -> Result { + ) -> Result { let mut auth = auth.clone(); if auth.id.is_none() { if auth.name.is_none() { diff --git a/crates/core/src/k8s_auth/auth.rs b/crates/core/src/k8s_auth/auth.rs index 80b02513..1b776e25 100644 --- a/crates/core/src/k8s_auth/auth.rs +++ b/crates/core/src/k8s_auth/auth.rs @@ -25,9 +25,8 @@ use tokio::fs; use tracing::{debug, trace}; use openstack_keystone_core_types::k8s_auth::*; -use openstack_keystone_core_types::token::TokenRestriction; -use crate::auth::AuthenticatedInfo; +use crate::auth::*; use crate::identity::IdentityApi; use crate::k8s_auth::{ K8sAuthApi, K8sAuthProviderError, service::K8sAuthService, types::K8sClaims, @@ -238,7 +237,7 @@ impl K8sAuthService { &self, state: &ServiceState, req: &K8sAuthRequest, - ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError> { + ) -> Result { let token_provider = state.provider.get_token_provider(); let identity_provider = state.provider.get_identity_provider(); // Fetch k8s auth instance. @@ -289,16 +288,21 @@ impl K8sAuthService { .get_user(state, user_id) .await? .ok_or(K8sAuthProviderError::UserNotFound(user_id.clone()))?; - Ok(( - AuthenticatedInfo { - methods: vec!["mapped".to_string()], - token_restriction_id: Some(role.token_restriction_id), - user: Some(user), - user_id: user_id.clone(), - ..Default::default() - }, - token_restriction, - )) + Ok(AuthenticationResultBuilder::default() + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user_id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .context(AuthenticationContext::K8s(K8sContext { + token_restriction_id: role.token_restriction_id.clone(), + })) + .token_restriction(token_restriction) + .build()?) } } @@ -1089,18 +1093,18 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= #[tokio::test] async fn test_auth() -> Result<()> { let mut token_mock = MockTokenProvider::default(); + let tr = TokenRestriction { + id: "trid".into(), + domain_id: "did".into(), + project_id: Some("pid".into()), + user_id: Some("uid".into()), + ..Default::default() + }; + let tr_clone = tr.clone(); token_mock .expect_get_token_restriction() .withf(|_, id: &'_ str, expand: &bool| id == "trid" && *expand) - .returning(|_, _, _| { - Ok(Some(TokenRestriction { - id: "trid".into(), - domain_id: "did".into(), - project_id: Some("pid".into()), - user_id: Some("uid".into()), - ..Default::default() - })) - }); + .returning(move |_, _, _| Ok(Some(tr_clone.clone()))); let mut identity_mock = MockIdentityProvider::default(); identity_mock @@ -1119,7 +1123,7 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= let (provider, state, token, _mock_server) = build_auth_test(token_mock, identity_mock).await?; - let (auth_info, tr) = provider + let auth_res = provider .authenticate_by_k8s_sa_token( &state, &K8sAuthRequest { @@ -1129,11 +1133,15 @@ FPrC1HpT3dzIAiEAtEB0so+KoJb/2Opn1RycVzxke1CQrWgjS8ySnnFK5ok= }, ) .await?; - assert_eq!("uid", auth_info.user_id); - assert_eq!(vec!["mapped".to_string()], auth_info.methods); - let trid = auth_info.token_restriction_id.unwrap(); - assert_eq!("trid".to_string(), trid); - assert_eq!(tr.id, trid); + match &auth_res.principal.identity { + IdentityInfo::User(user) => { + assert_eq!("uid", user.user_id); + } + _ => { + panic!("should result in a user"); + } + } + assert_eq!(tr, auth_res.token_restriction.unwrap()); Ok(()) } diff --git a/crates/core/src/k8s_auth/mock.rs b/crates/core/src/k8s_auth/mock.rs index 002bf44c..4ccfc6ef 100644 --- a/crates/core/src/k8s_auth/mock.rs +++ b/crates/core/src/k8s_auth/mock.rs @@ -16,9 +16,8 @@ use async_trait::async_trait; use mockall::mock; use openstack_keystone_core_types::k8s_auth::*; -use openstack_keystone_core_types::token::TokenRestriction; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::k8s_auth::{K8sAuthApi, K8sAuthProviderError}; use crate::keystone::ServiceState; @@ -33,7 +32,7 @@ mock! { &self, state: &ServiceState, req: &K8sAuthRequest, - ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError>; + ) -> Result; /// Register new K8s auth instance. async fn create_auth_instance( diff --git a/crates/core/src/k8s_auth/mod.rs b/crates/core/src/k8s_auth/mod.rs index 26417e80..4de149bf 100644 --- a/crates/core/src/k8s_auth/mod.rs +++ b/crates/core/src/k8s_auth/mod.rs @@ -26,9 +26,8 @@ mod types; use openstack_keystone_config::Config; use openstack_keystone_core_types::k8s_auth::*; -use openstack_keystone_core_types::token::TokenRestriction; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::k8s_auth::service::K8sAuthService; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManagerApi; @@ -78,7 +77,7 @@ impl K8sAuthApi for K8sAuthProvider { &self, state: &ServiceState, req: &K8sAuthRequest, - ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError> { + ) -> Result { match self { Self::Service(provider) => provider.authenticate_by_k8s_sa_token(state, req).await, #[cfg(any(test, feature = "mock"))] diff --git a/crates/core/src/k8s_auth/provider_api.rs b/crates/core/src/k8s_auth/provider_api.rs index 49011ea5..d3bffaf5 100644 --- a/crates/core/src/k8s_auth/provider_api.rs +++ b/crates/core/src/k8s_auth/provider_api.rs @@ -16,9 +16,8 @@ use async_trait::async_trait; use openstack_keystone_core_types::k8s_auth::*; -use openstack_keystone_core_types::token::TokenRestriction; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::k8s_auth::K8sAuthProviderError; use crate::keystone::ServiceState; @@ -38,7 +37,7 @@ pub trait K8sAuthApi: Send + Sync { &self, state: &ServiceState, req: &K8sAuthRequest, - ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError>; + ) -> Result; /// Register new K8s auth instance. /// diff --git a/crates/core/src/k8s_auth/service.rs b/crates/core/src/k8s_auth/service.rs index ded837c3..c3b2efdb 100644 --- a/crates/core/src/k8s_auth/service.rs +++ b/crates/core/src/k8s_auth/service.rs @@ -19,9 +19,8 @@ use async_trait::async_trait; use openstack_keystone_config::Config; use openstack_keystone_core_types::k8s_auth::*; -use openstack_keystone_core_types::token::TokenRestriction; -use crate::auth::AuthenticatedInfo; +use crate::auth::AuthenticationResult; use crate::common::{HttpClientPool, HttpClientProvider}; use crate::k8s_auth::{K8sAuthApi, K8sAuthProviderError, backend::K8sAuthBackend}; use crate::keystone::ServiceState; @@ -75,7 +74,7 @@ impl K8sAuthApi for K8sAuthService { &self, state: &ServiceState, req: &K8sAuthRequest, - ) -> Result<(AuthenticatedInfo, TokenRestriction), K8sAuthProviderError> { + ) -> Result { self.authenticate(state, req).await } diff --git a/crates/core/src/provider.rs b/crates/core/src/provider.rs index 3d8259fb..13a1dc6c 100644 --- a/crates/core/src/provider.rs +++ b/crates/core/src/provider.rs @@ -167,18 +167,18 @@ impl Provider { plugin_manager: &P, ) -> Result { let application_credential_provider = - ApplicationCredentialProvider::new(&cfg, plugin_manager)?; - let assignment_provider = AssignmentProvider::new(&cfg, plugin_manager)?; - let catalog_provider = CatalogProvider::new(&cfg, plugin_manager)?; - let federation_provider = FederationProvider::new(&cfg, plugin_manager)?; - let identity_provider = IdentityProvider::new(&cfg, plugin_manager)?; - let identity_mapping_provider = IdentityMappingProvider::new(&cfg, plugin_manager)?; - let k8s_auth_provider = K8sAuthProvider::new(&cfg, plugin_manager)?; - let resource_provider = ResourceProvider::new(&cfg, plugin_manager)?; - let revoke_provider = RevokeProvider::new(&cfg, plugin_manager)?; - let role_provider = RoleProvider::new(&cfg, plugin_manager)?; - let token_provider = TokenProvider::new(&cfg, plugin_manager)?; - let trust_provider = TrustProvider::new(&cfg, plugin_manager)?; + ApplicationCredentialProvider::new(cfg, plugin_manager)?; + let assignment_provider = AssignmentProvider::new(cfg, plugin_manager)?; + let catalog_provider = CatalogProvider::new(cfg, plugin_manager)?; + let federation_provider = FederationProvider::new(cfg, plugin_manager)?; + let identity_provider = IdentityProvider::new(cfg, plugin_manager)?; + let identity_mapping_provider = IdentityMappingProvider::new(cfg, plugin_manager)?; + let k8s_auth_provider = K8sAuthProvider::new(cfg, plugin_manager)?; + let resource_provider = ResourceProvider::new(cfg, plugin_manager)?; + let revoke_provider = RevokeProvider::new(cfg, plugin_manager)?; + let role_provider = RoleProvider::new(cfg, plugin_manager)?; + let token_provider = TokenProvider::new(cfg, plugin_manager)?; + let trust_provider = TrustProvider::new(cfg, plugin_manager)?; Ok(Self { application_credential: application_credential_provider, diff --git a/crates/core/src/token/mock.rs b/crates/core/src/token/mock.rs index 424a6ac3..697a2a4c 100644 --- a/crates/core/src/token/mock.rs +++ b/crates/core/src/token/mock.rs @@ -19,7 +19,7 @@ use mockall::mock; use openstack_keystone_core_types::token::*; use super::error::TokenProviderError; -use crate::auth::{AuthenticatedInfo, AuthzInfo}; +use crate::auth::{AuthenticationResult, AuthzInfo, SecurityContext}; use crate::keystone::ServiceState; use super::TokenApi; @@ -35,7 +35,7 @@ mock! { credential: &'a str, allow_expired: Option, window_seconds: Option, - ) -> Result; + ) -> Result; async fn validate_token<'a>( &self, @@ -48,9 +48,8 @@ mock! { #[mockall::concretize] fn issue_token( &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restriction: Option<&TokenRestriction> + security_context: &SecurityContext, + authz_info: &AuthzInfo, ) -> Result; fn encode_token(&self, token: &Token) -> Result; diff --git a/crates/core/src/token/mod.rs b/crates/core/src/token/mod.rs index e7c68a5c..60e5ef24 100644 --- a/crates/core/src/token/mod.rs +++ b/crates/core/src/token/mod.rs @@ -22,6 +22,7 @@ use async_trait::async_trait; use openstack_keystone_config::Config; +use openstack_keystone_core_types::auth::{AuthenticationResult, AuthzInfo, SecurityContext}; pub use openstack_keystone_core_types::token::*; pub mod backend; @@ -32,7 +33,6 @@ mod provider_api; pub mod service; mod validate; -use crate::auth::{AuthenticatedInfo, AuthzInfo}; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManagerApi; use crate::token::service::TokenService; @@ -86,7 +86,7 @@ impl TokenApi for TokenProvider { credential: &'a str, allow_expired: Option, window_seconds: Option, - ) -> Result { + ) -> Result { match self { Self::Service(provider) => { provider @@ -138,7 +138,7 @@ impl TokenApi for TokenProvider { /// Issue the Keystone token. /// /// # Parameters - /// - `authentication_info`: Information about the authenticated user. + /// - `security_context`: Information about the authenticated user. /// - `authz_info`: Authorization scope. /// - `token_restrictions`: Optional restrictions for the token. /// @@ -147,18 +147,13 @@ impl TokenApi for TokenProvider { #[tracing::instrument(level = "debug", skip(self))] fn issue_token( &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restrictions: Option<&TokenRestriction>, + security_context: &SecurityContext, + authz_info: &AuthzInfo, ) -> Result { match self { - Self::Service(provider) => { - provider.issue_token(authentication_info, authz_info, token_restrictions) - } + Self::Service(provider) => provider.issue_token(security_context, authz_info), #[cfg(any(test, feature = "mock"))] - Self::Mock(provider) => { - provider.issue_token(authentication_info, authz_info, token_restrictions) - } + Self::Mock(provider) => provider.issue_token(security_context, authz_info), } } diff --git a/crates/core/src/token/provider_api.rs b/crates/core/src/token/provider_api.rs index a56d608d..1b789935 100644 --- a/crates/core/src/token/provider_api.rs +++ b/crates/core/src/token/provider_api.rs @@ -17,7 +17,7 @@ use async_trait::async_trait; use openstack_keystone_core_types::token::*; -use crate::auth::{AuthenticatedInfo, AuthzInfo}; +use crate::auth::{AuthenticationResult, AuthzInfo, SecurityContext}; use crate::keystone::ServiceState; use crate::token::TokenProviderError; @@ -41,7 +41,7 @@ pub trait TokenApi: Send + Sync { credential: &'a str, allow_expired: Option, window_seconds: Option, - ) -> Result; + ) -> Result; /// Validate the token. /// @@ -64,7 +64,7 @@ pub trait TokenApi: Send + Sync { /// Issue a token for given parameters. /// /// # Parameters - /// - `authentication_info`: Authentication information for the token. + /// - `security_context`: Authentication information for the token. /// - `authz_info`: Authorization information (scope) for the token. /// - `token_restriction`: Optional restrictions for the token. /// @@ -72,9 +72,8 @@ pub trait TokenApi: Send + Sync { /// - `Result` - The issued token or an error. fn issue_token( &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restriction: Option<&TokenRestriction>, + security_context: &SecurityContext, + authz_info: &AuthzInfo, ) -> Result; /// Encode the token into the X-Subject-Token String. diff --git a/crates/core/src/token/service.rs b/crates/core/src/token/service.rs index 6d55fd56..12f59557 100644 --- a/crates/core/src/token/service.rs +++ b/crates/core/src/token/service.rs @@ -20,7 +20,6 @@ //! solution. use async_trait::async_trait; -use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use chrono::{DateTime, TimeDelta, Utc}; use std::collections::HashSet; use std::sync::Arc; @@ -31,13 +30,10 @@ use openstack_keystone_config::Config; use openstack_keystone_core_types::assignment::{ RoleAssignmentListParameters, RoleAssignmentListParametersBuilder, }; -use openstack_keystone_core_types::resource::{Domain, Project}; use openstack_keystone_core_types::role::RoleRef; -use openstack_keystone_core_types::token::payload::*; use openstack_keystone_core_types::token::*; -use openstack_keystone_core_types::trust::*; -use crate::auth::{AuthenticatedInfo, AuthenticationError, AuthzInfo}; +use crate::auth::*; use crate::identity::IdentityApi; use crate::keystone::ServiceState; use crate::plugin_manager::PluginManagerApi; @@ -108,329 +104,6 @@ impl TokenService { .unwrap_or(default_expiry)) } - /// Create unscoped token. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_unscoped_token( - &self, - authentication_info: &AuthenticatedInfo, - ) -> Result { - Ok(Token::Unscoped( - UnscopedPayloadBuilder::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()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .build()?, - )) - } - - /// Create project scoped token. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - `project`: The project to scope the token to. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_project_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - project: &Project, - ) -> Result { - let token_expiry = self.get_new_token_expiry(&authentication_info.expires_at)?; - if let Some(application_credential) = &authentication_info.application_credential { - // Token for the application credential authentication - Ok(Token::ApplicationCredential( - ApplicationCredentialPayloadBuilder::default() - .application_credential_id(application_credential.id.clone()) - .application_credential(application_credential.clone()) - .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()) - .expires_at( - application_credential - .expires_at - .map(|ac_expiry| std::cmp::min(token_expiry, ac_expiry)) - .unwrap_or(token_expiry), - ) - .project_id(project.id.clone()) - .project(project.clone()) - .build()?, - )) - } else { - // General project scoped token - Ok(Token::ProjectScope( - ProjectScopePayloadBuilder::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()) - .expires_at(token_expiry) - .project_id(project.id.clone()) - .project(project.clone()) - .build()?, - )) - } - } - - /// Create domain scoped token. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - `domain`: The domain to scope the token to. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_domain_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - domain: &Domain, - ) -> Result { - Ok(Token::DomainScope( - DomainScopePayloadBuilder::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()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .domain_id(domain.id.clone()) - .domain(domain.clone()) - .build()?, - )) - } - - /// Create unscoped token with the identity provider bind. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_federated_unscoped_token( - &self, - authentication_info: &AuthenticatedInfo, - ) -> Result { - if let (Some(idp_id), Some(protocol_id)) = ( - authentication_info.idp_id.clone(), - authentication_info.protocol_id.clone(), - ) { - Ok(Token::FederationUnscoped( - FederationUnscopedPayloadBuilder::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()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .idp_id(idp_id) - .protocol_id(protocol_id) - .group_ids(vec![]) - .build()?, - )) - } else { - Err(TokenProviderError::FederatedPayloadMissingData) - } - } - - /// Create project scoped token with the identity provider bind. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - `project`: The project to scope the token to. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_federated_project_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - project: &Project, - ) -> Result { - if let (Some(idp_id), Some(protocol_id)) = ( - authentication_info.idp_id.clone(), - authentication_info.protocol_id.clone(), - ) { - Ok(Token::FederationProjectScope( - FederationProjectScopePayloadBuilder::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()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .idp_id(idp_id) - .protocol_id(protocol_id) - .group_ids( - authentication_info - .user_groups - .clone() - .iter() - .map(|grp| grp.id.clone()) - .collect::>(), - ) - .project_id(project.id.clone()) - .project(project.clone()) - .build()?, - )) - } else { - Err(TokenProviderError::FederatedPayloadMissingData) - } - } - - /// Create domain scoped token with the identity provider bind. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - `domain`: The domain to scope the token to. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_federated_domain_scope_token( - &self, - authentication_info: &AuthenticatedInfo, - domain: &Domain, - ) -> Result { - if let (Some(idp_id), Some(protocol_id)) = ( - authentication_info.idp_id.clone(), - authentication_info.protocol_id.clone(), - ) { - Ok(Token::FederationDomainScope( - FederationDomainScopePayloadBuilder::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()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .idp_id(idp_id) - .protocol_id(protocol_id) - .group_ids( - authentication_info - .user_groups - .clone() - .iter() - .map(|grp| grp.id.clone()) - .collect::>(), - ) - .domain_id(domain.id.clone()) - .domain(domain.clone()) - .build()?, - )) - } else { - Err(TokenProviderError::FederatedPayloadMissingData) - } - } - - /// Create token with the specified restrictions. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - `authz_info`: Authorization information. - /// - `restriction`: The restrictions to apply to the token. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_restricted_token( - &self, - authentication_info: &AuthenticatedInfo, - authz_info: &AuthzInfo, - restriction: &TokenRestriction, - ) -> Result { - Ok(Token::Restricted( - RestrictedPayloadBuilder::default() - .user_id( - restriction - .user_id - .as_ref() - .unwrap_or(&authentication_info.user_id.clone()), - ) - .user(authentication_info.user.clone()) - .methods(authentication_info.methods.clone().iter()) - .audit_ids(authentication_info.audit_ids.clone().iter()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .token_restriction_id(restriction.id.clone()) - .project_id( - restriction - .project_id - .as_ref() - .or(match authz_info { - AuthzInfo::Project(project) => Some(&project.id), - _ => None, - }) - .ok_or_else(|| TokenProviderError::RestrictedTokenNotProjectScoped)?, - ) - .allow_renew(restriction.allow_renew) - .allow_rescope(restriction.allow_rescope) - .roles(restriction.roles.clone()) - .build()?, - )) - } - - /// Create system scoped token. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - 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(&authentication_info.expires_at)?) - .build()?, - )) - } - - /// Create token based on the trust. - /// - /// # Parameters - /// - `authentication_info`: Information about the authenticated user. - /// - `trust`: The trust relationship to use. - /// - /// # Returns - /// - `Result` - The issued `Token` or an error. - fn create_trust_token( - &self, - authentication_info: &AuthenticatedInfo, - trust: &Trust, - ) -> Result { - if let Some(project_id) = &trust.project_id { - Ok(Token::Trust( - TrustPayloadBuilder::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()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .trust_id(trust.id.clone()) - .project_id(project_id.clone()) - .build()?, - )) - } else { - // Trust without project_id is unscoped - Ok(Token::Unscoped( - UnscopedPayloadBuilder::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()) - .expires_at(self.get_new_token_expiry(&authentication_info.expires_at)?) - .build()?, - )) - } - } - /// Expand user information in the token. /// /// # Parameters @@ -510,16 +183,14 @@ impl TokenService { token: &mut Token, ) -> Result<(), TokenProviderError> { match token { - Token::ProjectScope(data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; - } + Token::ProjectScope(data) if data.project.is_none() => { + let project = state + .provider + .get_resource_provider() + .get_project(state, &data.project_id) + .await?; + + data.project = project; } Token::ApplicationCredential(data) => { if data.application_credential.is_none() { @@ -546,49 +217,41 @@ impl TokenService { data.project = project; } } - Token::FederationProjectScope(data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; - } + Token::FederationProjectScope(data) if data.project.is_none() => { + let project = state + .provider + .get_resource_provider() + .get_project(state, &data.project_id) + .await?; + + data.project = project; } - Token::DomainScope(data) => { - if data.domain.is_none() { - let domain = state - .provider - .get_resource_provider() - .get_domain(state, &data.domain_id) - .await?; - - data.domain = domain; - } + Token::DomainScope(data) if data.domain.is_none() => { + let domain = state + .provider + .get_resource_provider() + .get_domain(state, &data.domain_id) + .await?; + + data.domain = domain; } - Token::FederationDomainScope(data) => { - if data.domain.is_none() { - let domain = state - .provider - .get_resource_provider() - .get_domain(state, &data.domain_id) - .await?; - - data.domain = domain; - } + Token::FederationDomainScope(data) if data.domain.is_none() => { + let domain = state + .provider + .get_resource_provider() + .get_domain(state, &data.domain_id) + .await?; + + data.domain = domain; } - Token::Restricted(data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; - } + Token::Restricted(data) if data.project.is_none() => { + let project = state + .provider + .get_resource_provider() + .get_project(state, &data.project_id) + .await?; + + data.project = project; } Token::SystemScope(_data) => {} Token::Trust(data) => { @@ -787,15 +450,13 @@ impl TokenService { 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::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( @@ -896,27 +557,48 @@ impl TokenApi for TokenService { credential: &'a str, allow_expired: Option, window_seconds: Option, - ) -> Result { + ) -> Result { // TODO: is the expand really false? let token = self .validate_token(state, credential, allow_expired, window_seconds) .await?; - if let Token::Restricted(restriction) = &token - && !restriction.allow_renew - { - return Err(AuthenticationError::TokenRenewalForbidden)?; - } - let mut auth_info_builder = AuthenticatedInfo::builder(); - auth_info_builder.user_id(token.user_id()); - auth_info_builder.methods(token.methods().clone()); - auth_info_builder.audit_ids(token.audit_ids().clone()); - auth_info_builder.expires_at(*token.expires_at()); + let user = token + .user() + .as_ref() + .ok_or(TokenProviderError::UserNotFound(token.user_id().clone()))?; + let mut ctx = AuthenticationResultBuilder::default(); + + ctx.context(AuthenticationContext::Token( + TokenContextBuilder::default() + .audit_ids(token.audit_ids().clone()) + .methods(token.methods().clone()) + .expires_at(*token.expires_at()) + .build()?, + )) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(token.user_id()) + .user(user.clone()) + .build()?, + ), + }); if let Token::Restricted(restriction) = &token { - auth_info_builder.token_restriction_id(restriction.token_restriction_id.clone()); + if !restriction.allow_renew { + return Err(AuthenticationError::TokenRenewalForbidden)?; + } + let token_restriction = &state + .provider + .get_token_provider() + .get_token_restriction(state, &restriction.token_restriction_id, false) + .await? + .ok_or(TokenProviderError::TokenRestrictionNotFound( + restriction.token_restriction_id.clone(), + ))?; + ctx.token_restriction(token_restriction.to_owned()); } - Ok(auth_info_builder - .build() - .map_err(AuthenticationError::from)?) + Ok(ctx.build().map_err(AuthenticationError::from)?) } /// Validate token. @@ -969,6 +651,22 @@ impl TokenApi for TokenService { /// Issue the Keystone token. /// + /// # Security Note + /// A series of checks is performed to verify whether a token for the + /// requested scope can be issued with the given [`SecurityContext`]. + /// + /// * `token_restriction` - When set in the context only a restricted token + /// bound to the project scope can be issued. + /// * `trust` - For Trust scope only Password/Token auth can be used. Trust + /// from Trust is + /// forbidden. + /// * `system` - Only Password/Token auth can be used to grant system scope. + /// * `application_credentials` - Token can be granted only when auth is + /// scoped to AppCreds and + /// the project_id matches the scope. No other token can be issued with + /// [`ApplicationCredential`] in the context. + /// + /// /// # Parameters /// - `authentication_info`: Information about the authenticated user. /// - `authz_info`: Authorization scope. @@ -978,56 +676,23 @@ impl TokenApi for TokenService { /// - `Result` - The issued token or an error. fn issue_token( &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restrictions: Option<&TokenRestriction>, + ctx: &SecurityContext, + authz_info: &AuthzInfo, ) -> Result { // This should be executed already, but let's better repeat it as last line of - // defence. It is also necessary to call this before to stop before we + // defense. It is also necessary to call this before to stop before we // start to resolve authz info. - authentication_info.validate()?; + ctx.validate()?; - // TODO: Check whether it is allowed to change the scope of the token if + // Check whether it is allowed to change the scope of the token if // AuthenticatedInfo already contains scope it was issued for. - let mut authentication_info = authentication_info; - authentication_info - .audit_ids - .push(URL_SAFE_NO_PAD.encode(Uuid::new_v4().as_bytes())); - if let Some(token_restrictions) = &token_restrictions { - self.create_restricted_token(&authentication_info, &authz_info, token_restrictions) - } else if authentication_info.idp_id.is_some() && authentication_info.protocol_id.is_some() - { - match &authz_info { - AuthzInfo::Domain(domain) => { - self.create_federated_domain_scope_token(&authentication_info, domain) - } - AuthzInfo::Project(project) => { - self.create_federated_project_scope_token(&authentication_info, project) - } - AuthzInfo::Trust(_trust) => Err(TokenProviderError::Conflict { - 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 { - match &authz_info { - AuthzInfo::Domain(domain) => { - self.create_domain_scope_token(&authentication_info, domain) - } - AuthzInfo::Project(project) => { - 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), - } - } + ctx.validate_scope_boundaries(authz_info)?; + let token = Token::from_security_context_with_scope( + ctx, + authz_info, + self.get_new_token_expiry(&ctx.expires_at)?, + )?; + Ok(token) } /// Encode the token into a `String` representation. @@ -1200,13 +865,11 @@ mod tests { use openstack_keystone_core_types::assignment::*; use openstack_keystone_core_types::identity::UserResponseBuilder; use openstack_keystone_core_types::resource::*; - use openstack_keystone_core_types::trust::*; use super::super::tests::setup_config; use super::*; use crate::application_credential::MockApplicationCredentialProvider; use crate::assignment::MockAssignmentProvider; - use crate::auth::AuthenticatedInfoBuilder; use crate::identity::MockIdentityProvider; use crate::provider::Provider; use crate::resource::MockResourceProvider; @@ -1591,268 +1254,4 @@ mod tests { ); } } - - #[tokio::test] - async fn test_create_unscoped_token() { - let token_provider = get_provider(&Config::default(), None); - let now = Utc::now(); - let token = token_provider - .create_unscoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_unscoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - assert!(token.project_id().is_none()); - } - - #[tokio::test] - async fn test_create_project_scope_token() { - let token_provider = get_provider(&Config::default(), None); - let now = Utc::now(); - let token = token_provider - .create_project_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &ProjectBuilder::default() - .id("pid") - .domain_id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_project_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &ProjectBuilder::default() - .id("pid") - .domain_id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - assert_eq!(*token.project_id().unwrap(), "pid"); - } - - #[tokio::test] - async fn test_create_domain_scope_token() { - let token_provider = get_provider(&Config::default(), None); - let now = Utc::now(); - let token = token_provider - .create_domain_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &DomainBuilder::default() - .id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_domain_scope_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &DomainBuilder::default() - .id("did") - .name("pname") - .enabled(true) - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - assert_eq!(token.domain().unwrap().id, "did"); - } - - #[tokio::test] - async fn test_create_system_token() { - let token_provider = get_provider(&Config::default(), None); - let now = Utc::now(); - let token = token_provider - .create_system_scoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_system_scoped_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::SystemScope(data) = token { - assert_eq!(data.system_id, "system"); - } else { - panic!("wrong token type"); - } - } - - #[tokio::test] - async fn test_create_trust_token() { - let token_provider = get_provider(&Config::default(), None); - let now = Utc::now(); - let token = token_provider - .create_trust_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &TrustBuilder::default() - .id("tid") - .impersonation(false) - .trustor_user_id("trustor_uid") - .trustee_user_id("trustor_uid") - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_trust_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &TrustBuilder::default() - .id("tid") - .impersonation(false) - .trustor_user_id("trustor_uid") - .trustee_user_id("trustor_uid") - .project_id("pid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::Trust(data) = token { - assert_eq!(data.trust_id, "tid"); - } else { - panic!("wrong token type"); - } - - // unscoped - let token = token_provider - .create_trust_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &TrustBuilder::default() - .id("tid") - .impersonation(false) - .trustor_user_id("trustor_uid") - .trustee_user_id("trustor_uid") - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::Unscoped(_data) = token { - } else { - panic!("wrong token type"); - } - } - - #[tokio::test] - async fn test_create_restricted_token() { - let token_provider = get_provider(&Config::default(), None); - let now = Utc::now(); - let token = token_provider - .create_restricted_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .expires_at(now) - .build() - .unwrap(), - &AuthzInfo::System, - &TokenRestrictionBuilder::default() - .id("rid") - .domain_id("did") - .project_id("pid") - .allow_renew(true) - .allow_rescope(true) - .role_ids([]) - .build() - .unwrap(), - ) - .unwrap(); - assert_eq!(*token.expires_at(), now); - assert_eq!(*token.user_id(), "uid"); - let token = token_provider - .create_restricted_token( - &AuthenticatedInfoBuilder::default() - .user_id("uid") - .build() - .unwrap(), - &AuthzInfo::System, - &TokenRestrictionBuilder::default() - .id("rid") - .domain_id("did") - .project_id("pid") - .allow_renew(true) - .allow_rescope(true) - .role_ids([]) - .build() - .unwrap(), - ) - .unwrap(); - assert!(now < *token.expires_at()); - assert_eq!(*token.user_id(), "uid"); - if let Token::Restricted(data) = token { - assert_eq!(data.token_restriction_id, "rid"); - } else { - panic!("wrong token type"); - } - } } diff --git a/crates/identity-sql/src/authenticate.rs b/crates/identity-sql/src/authenticate.rs index 14fa904c..0aa11402 100644 --- a/crates/identity-sql/src/authenticate.rs +++ b/crates/identity-sql/src/authenticate.rs @@ -17,7 +17,7 @@ use sea_orm::DatabaseConnection; use tracing::info; use openstack_keystone_config::Config; -use openstack_keystone_core::auth::{AuthenticatedInfo, AuthenticationError}; +use openstack_keystone_core::auth::*; use openstack_keystone_core::common::password_hashing; use openstack_keystone_core::identity::IdentityProviderError; use openstack_keystone_core_types::identity::{UserPasswordAuthRequest, UserResponseBuilder}; @@ -57,7 +57,7 @@ pub async fn authenticate_by_password( config: &Config, db: &DatabaseConnection, auth: &UserPasswordAuthRequest, -) -> Result { +) -> Result { let user_with_passwords = local_user::load_local_user_with_passwords( db, auth.id.as_ref(), @@ -132,12 +132,25 @@ pub async fn authenticate_by_password( .merge_passwords_data(passwords) .build()?; - Ok(AuthenticatedInfo::builder() - .user_id(user_entry.id.clone()) - .user(user_entry) - .methods(vec!["password".into()]) - .build() - .map_err(AuthenticationError::from)?) + Ok(AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user_entry.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user_entry.id.clone()) + .user(user_entry.clone()) + .build()?, + ), + }) + .build()?) + + //Ok(AuthenticatedInfo::builder() + // .user_id(user_entry.id.clone()) + // .user(user_entry) + // .methods(vec!["password".into()]) + // .build() + // .map_err(AuthenticationError::from)?) } /// Verify whether the account is temporarily locked according to the security diff --git a/crates/identity-sql/src/lib.rs b/crates/identity-sql/src/lib.rs index a354ae9b..b7843abb 100644 --- a/crates/identity-sql/src/lib.rs +++ b/crates/identity-sql/src/lib.rs @@ -18,7 +18,7 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use sea_orm::{DatabaseConnection, Schema, sea_query::Index}; -use openstack_keystone_core::auth::AuthenticatedInfo; +use openstack_keystone_core::auth::AuthenticationResult; use openstack_keystone_core::identity::IdentityProviderError; use openstack_keystone_core::identity::backend::IdentityBackend; use openstack_keystone_core::keystone::ServiceState; @@ -143,7 +143,7 @@ impl IdentityBackend for SqlBackend { &self, state: &ServiceState, auth: &UserPasswordAuthRequest, - ) -> Result { + ) -> Result { let config = state.config_manager.config.read().await; Ok(authenticate::authenticate_by_password(&config, &state.db, auth).await?) } diff --git a/crates/k8s-auth-raft/src/lib.rs b/crates/k8s-auth-raft/src/lib.rs index 7c8e8bdb..cb86f73e 100644 --- a/crates/k8s-auth-raft/src/lib.rs +++ b/crates/k8s-auth-raft/src/lib.rs @@ -45,7 +45,7 @@ impl RaftBackend { /// # Returns /// The prefix key. fn get_auth_instance_prefix(&self) -> String { - format!("k8s_auth:instance:id:") + "k8s_auth:instance:id:".to_string() } /// Get the storage key for auth instance - domain based index. @@ -95,7 +95,7 @@ impl RaftBackend { /// # Returns /// The prefix key. fn get_auth_role_prefix(&self) -> String { - format!("k8s_auth:role:id:") + "k8s_auth:role:id:".to_string() } /// Get the storage key for auth role - domain based index. @@ -431,10 +431,9 @@ impl K8sAuthBackend for RaftBackend { .await .map_err(K8sAuthProviderError::raft)? .map(|x| x.data) + && post_filters.iter().all(|f| f.matches(&candidate)) { - if post_filters.iter().all(|f| f.matches(&candidate)) { - res.push(candidate); - } + res.push(candidate); } } } else { @@ -531,10 +530,9 @@ impl K8sAuthBackend for RaftBackend { .await .map_err(K8sAuthProviderError::raft)? .map(|x| x.data) + && post_filters.iter().all(|f| f.matches(&candidate)) { - if post_filters.iter().all(|f| f.matches(&candidate)) { - res.push(candidate); - } + res.push(candidate); } } } else { diff --git a/crates/keystone/Cargo.toml b/crates/keystone/Cargo.toml index fcbb8f20..36dd7034 100644 --- a/crates/keystone/Cargo.toml +++ b/crates/keystone/Cargo.toml @@ -36,7 +36,7 @@ openstack-keystone-appcred-sql = { version = "0.1", path = "../appcred-sql/" } openstack-keystone-assignment-sql = { version = "0.1", path = "../assignment-sql/" } openstack-keystone-catalog-sql = { version = "0.1", path = "../catalog-sql/" } openstack-keystone-config = { version = "0.1", path = "../config" } -openstack-keystone-core = { version = "0.1", path = "../core" } +openstack-keystone-core = { version = "0.1", path = "../core", features = ["api"] } openstack-keystone-core-types = { version = "0.1", path = "../core-types" } openstack-keystone-distributed-storage = { version = "0.1", path = "../storage/"} openstack-keystone-federation-sql = { version = "0.1", path = "../federation-sql" } diff --git a/crates/keystone/src/api/common.rs b/crates/keystone/src/api/common.rs index 963b0a42..66fb58af 100644 --- a/crates/keystone/src/api/common.rs +++ b/crates/keystone/src/api/common.rs @@ -17,12 +17,9 @@ use url::Url; use openstack_keystone_api_types::Link; use openstack_keystone_config::Config; -use openstack_keystone_core_types::resource::{Domain, Project}; -use openstack_keystone_core_types::scope::Scope as ProviderScope; +use openstack_keystone_core_types::resource::Domain; use crate::api::KeystoneApiError; -use crate::api::types::ScopeProject; -use crate::auth::AuthzInfo; use crate::keystone::ServiceState; use crate::resource::ResourceApi; @@ -65,100 +62,6 @@ pub async fn get_domain, N: AsRef>( } } -/// Find the project referred in the scope. -/// -/// # Arguments -/// * `state` - The service state. -/// * `scope` - The scope to find the project. -/// -/// # Returns -/// The resolved project. -pub async fn find_project_from_scope( - state: &ServiceState, - scope: &ScopeProject, -) -> Result, KeystoneApiError> { - let project = if let Some(pid) = &scope.id { - state - .provider - .get_resource_provider() - .get_project(state, pid) - .await? - } else if let Some(name) = &scope.name { - if let Some(domain) = &scope.domain { - let domain_id = match &domain.id { - Some(id) => id.clone(), - None => { - state - .provider - .get_resource_provider() - .find_domain_by_name( - state, - &domain - .name - .clone() - .ok_or(KeystoneApiError::DomainIdOrName)?, - ) - .await? - .ok_or(KeystoneApiError::NotFound { - resource: "domain".to_string(), - identifier: domain - .name - .clone() - .ok_or(KeystoneApiError::DomainIdOrName)?, - })? - .id - } - }; - state - .provider - .get_resource_provider() - .get_project_by_name(state, name, &domain_id) - .await? - } else { - return Err(KeystoneApiError::ProjectDomain); - } - } else { - return Err(KeystoneApiError::ProjectIdOrName); - }; - Ok(project) -} - -/// Convert [ProviderScope] to [AuthzInfo]. -/// -/// # Arguments -/// * `state`: The service state -/// * `scope`: The scope to extract the AuthZ information from -/// -/// # Returns -/// * `Ok(AuthzInfo)`: The AuthZ information -/// * `Err(KeystoneApiError)`: An error if the scope is not valid -#[tracing::instrument(skip(state), err)] -pub async fn get_authz_info( - state: &ServiceState, - scope: Option<&ProviderScope>, -) -> Result { - let authz_info = match scope { - Some(ProviderScope::Project(scope)) => { - if let Some(project) = find_project_from_scope(state, &scope.into()).await? { - AuthzInfo::Project(project) - } else { - return Err(KeystoneApiError::UnauthorizedNoContext); - } - } - Some(ProviderScope::Domain(scope)) => { - if let Ok(domain) = get_domain(state, scope.id.as_ref(), scope.name.as_ref()).await { - AuthzInfo::Domain(domain) - } else { - return Err(KeystoneApiError::UnauthorizedNoContext); - } - } - Some(ProviderScope::System(_scope)) => todo!(), - None => AuthzInfo::Unscoped, - }; - authz_info.validate()?; - Ok(authz_info) -} - /// Prepare the links for the paginated resource collection. pub fn build_pagination_links( config: &Config, diff --git a/crates/keystone/src/api/v3/auth/token/common.rs b/crates/keystone/src/api/v3/auth/token/common.rs index e1684bd6..035a0647 100644 --- a/crates/keystone/src/api/v3/auth/token/common.rs +++ b/crates/keystone/src/api/v3/auth/token/common.rs @@ -12,13 +12,11 @@ // // SPDX-License-Identifier: Apache-2.0 +use openstack_keystone_core::api::common::find_project_from_scope; + use crate::api::v3::auth::token::types::AuthRequest; -use crate::api::{ - Scope, - common::{find_project_from_scope, get_domain}, - error::KeystoneApiError, -}; -use crate::auth::{AuthenticatedInfo, AuthzInfo}; +use crate::api::{Scope, common::get_domain, error::KeystoneApiError}; +use crate::auth::*; use crate::identity::IdentityApi; use crate::keystone::ServiceState; use crate::token::TokenApi; @@ -31,13 +29,13 @@ use crate::token::TokenApi; pub(super) async fn authenticate_request( state: &ServiceState, req: &AuthRequest, -) -> Result { - let mut authenticated_info: Option = None; +) -> Result, KeystoneApiError> { + let mut res = Vec::new(); for method in req.auth.identity.methods.iter() { if method == "password" { if let Some(password_auth) = &req.auth.identity.password { let req = password_auth.user.clone().try_into()?; - authenticated_info = Some( + res.push( state .provider .get_identity_provider() @@ -48,36 +46,35 @@ pub(super) async fn authenticate_request( } else if method == "token" && let Some(token) = &req.auth.identity.token { - let mut authz = state + let mut auth_res = state .provider .get_token_provider() .authenticate_by_token(state, &token.id, Some(false), None) .await?; - // Resolve the user - authz.user = Some( - state - .provider - .get_identity_provider() - .get_user(state, &authz.user_id) - .await - .map(|x| { - x.ok_or_else(|| KeystoneApiError::NotFound { - resource: "user".into(), - identifier: authz.user_id.clone(), - }) - })??, - ); - authenticated_info = Some(authz); + if let IdentityInfo::User(ref mut identity) = auth_res.principal.identity { + identity.user = Some( + state + .provider + .get_identity_provider() + .get_user(state, &identity.user_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "user".into(), + identifier: identity.user_id.clone(), + }) + })??, + ); + }; + res.push(auth_res); {} } } - authenticated_info - .ok_or(KeystoneApiError::UnauthorizedNoContext) - .and_then(|authn| { - authn.validate()?; - Ok(authn) - }) + if res.is_empty() { + return Err(KeystoneApiError::UnauthorizedNoContext); + } + Ok(res) } /// Build the AuthZ information from the request. @@ -111,7 +108,7 @@ pub(super) async fn get_authz_info( return Err(KeystoneApiError::UnauthorizedNoContext); } } - Some(Scope::System(_scope)) => AuthzInfo::System, + Some(Scope::System(_scope)) => AuthzInfo::System("system".into()), None => AuthzInfo::Unscoped, }; authz_info.validate()?; @@ -120,33 +117,33 @@ pub(super) async fn get_authz_info( #[cfg(test)] mod tests { + use openstack_keystone_core_types::auth::*; use openstack_keystone_core_types::identity::{UserPasswordAuthRequest, UserResponseBuilder}; use super::super::types::*; use super::*; use crate::api::KeystoneApiError; use crate::api::tests::get_mocked_state; - use crate::auth::AuthenticatedInfo; use crate::identity::MockIdentityProvider; use crate::provider::Provider; use crate::token::MockTokenProvider; #[tokio::test] async fn test_authenticate_request_password() { - let auth_info = AuthenticatedInfo::builder() - .user_id("uid") - .user( - UserResponseBuilder::default() - .id("uid") - .domain_id("udid") - .enabled(true) - .name("name") - .build() - .unwrap(), - ) + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) .build() .unwrap(); - let auth_clone = auth_info.clone(); + let auth_clone = auth.clone(); let mut identity_mock = MockIdentityProvider::default(); identity_mock .expect_authenticate_by_password() @@ -162,7 +159,7 @@ mod tests { let state = get_mocked_state(provider, true, None, None).await; assert_eq!( - auth_info, + vec![auth], authenticate_request( &state, &AuthRequest { @@ -190,7 +187,29 @@ mod tests { #[tokio::test] async fn test_authenticate_request_token() { + let user = UserResponseBuilder::default() + .id("uid") + .domain_id("user_domain_id") + .enabled(true) + .name("name") + .build() + .unwrap(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Token(TokenContext::default())) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .user(user.clone()) + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); let mut token_mock = MockTokenProvider::default(); + let auth_clone = auth.clone(); token_mock .expect_authenticate_by_token() .withf( @@ -198,24 +217,12 @@ mod tests { id == "fake_token" && *allow_expired == Some(false) && window.is_none() }, ) - .returning(|_, _, _, _| { - Ok(AuthenticatedInfo::builder().user_id("uid").build().unwrap()) - }); + .returning(move |_, _, _, _| Ok(auth_clone.clone())); let mut identity_mock = MockIdentityProvider::default(); identity_mock .expect_get_user() .withf(|_, id: &'_ str| id == "uid") - .returning(|_, id: &'_ str| { - Ok(Some( - UserResponseBuilder::default() - .id(id) - .domain_id("user_domain_id") - .enabled(true) - .name("name") - .build() - .unwrap(), - )) - }); + .returning(move |_, _| Ok(Some(user.clone()))); let provider = Provider::mocked_builder() .mock_identity(identity_mock) @@ -224,19 +231,7 @@ mod tests { let state = get_mocked_state(provider, true, None, Some(true)).await; assert_eq!( - AuthenticatedInfo::builder() - .user_id("uid") - .user( - UserResponseBuilder::default() - .id("uid") - .domain_id("user_domain_id") - .enabled(true) - .name("name") - .build() - .unwrap(), - ) - .build() - .unwrap(), + vec![auth], authenticate_request( &state, &AuthRequest { diff --git a/crates/keystone/src/api/v3/auth/token/create.rs b/crates/keystone/src/api/v3/auth/token/create.rs index e78d3566..71d2e6e5 100644 --- a/crates/keystone/src/api/v3/auth/token/create.rs +++ b/crates/keystone/src/api/v3/auth/token/create.rs @@ -22,6 +22,7 @@ use axum::{ use validator::Validate; use openstack_keystone_core::api::v3::auth::token::token_impl::build_api_token_v3; +use openstack_keystone_core_types::auth::*; use crate::api::v3::auth::token::common::{authenticate_request, get_authz_info}; use crate::api::v3::auth::token::types::{AuthRequest, CreateTokenParameters, TokenResponse}; @@ -48,27 +49,22 @@ pub(super) async fn create( Json(req): Json, ) -> Result { req.validate()?; - let authed_info = authenticate_request(&state, &req).await?; + let auth_res = authenticate_request(&state, &req).await?; + let ctx = SecurityContext::try_from(auth_res)?; let authz_info = get_authz_info(&state, &req).await?; - if let Some(restriction_id) = &authed_info.token_restriction_id { - let restriction = state - .provider - .get_token_provider() - .get_token_restriction(&state, restriction_id, true) - .await? - .ok_or(KeystoneApiError::InternalError( - "token restriction {restriction_id} not found".to_string(), - ))?; - if !restriction.allow_rescope && req.auth.scope.is_some() { - return Err(KeystoneApiError::AuthenticationRescopeForbidden); - } + // This is a new authentication/reauthentication. Check if that is allowed at + // all + if let Some(token_restriction) = &ctx.token_restriction + && !token_restriction.allow_rescope + && req.auth.scope.is_some() + { + return Err(KeystoneApiError::AuthenticationRescopeForbidden); } - let mut token = - state - .provider - .get_token_provider() - .issue_token(authed_info, authz_info, None)?; + let mut token = state + .provider + .get_token_provider() + .issue_token(&ctx, &authz_info)?; token = state .provider @@ -123,13 +119,13 @@ mod tests { use tracing_test::traced_test; use openstack_keystone_config::{Config, ConfigManager}; + use openstack_keystone_core_types::auth::*; use openstack_keystone_core_types::identity::{UserPasswordAuthRequest, UserResponseBuilder}; use openstack_keystone_core_types::resource::{Domain, Project}; use openstack_keystone_core_types::token::{ProjectScopePayload, Token as ProviderToken}; use crate::api::v3::auth::token::types::*; use crate::assignment::MockAssignmentProvider; - use crate::auth::AuthenticatedInfo; use crate::catalog::MockCatalogProvider; use crate::identity::MockIdentityProvider; use crate::keystone::Service; @@ -166,6 +162,20 @@ mod tests { .expect_list_role_assignments() .returning(|_, _| Ok(Vec::new())); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let mut identity_mock = MockIdentityProvider::default(); identity_mock .expect_authenticate_by_password() @@ -174,21 +184,7 @@ mod tests { && req.password == "pass" && req.name == Some("uname".to_string()) }) - .returning(|_, _| { - Ok(AuthenticatedInfo::builder() - .user_id("uid") - .user( - UserResponseBuilder::default() - .id("uid") - .domain_id("udid") - .enabled(true) - .name("name") - .build() - .unwrap(), - ) - .build() - .unwrap()) - }); + .returning(move |_, _| Ok(auth.clone())); let mut resource_mock = MockResourceProvider::default(); resource_mock @@ -204,7 +200,7 @@ mod tests { .withf(|_, id: &'_ str| id == "pdid") .returning(move |_, _| Ok(Some(project_domain.clone()))); let mut token_mock = MockTokenProvider::default(); - token_mock.expect_issue_token().returning(|_, _, _| { + token_mock.expect_issue_token().returning(|_, _| { Ok(ProviderToken::ProjectScope(ProjectScopePayload { user_id: "bar".into(), methods: Vec::from(["password".to_string()]), @@ -334,23 +330,23 @@ mod tests { async fn test_post_project_disabled() { let config = Config::default(); let mut identity_mock = MockIdentityProvider::default(); + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some("did".into()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id("uid") + .build() + .unwrap(), + ), + }) + .build() + .unwrap(); + let auth_clone = auth.clone(); identity_mock .expect_authenticate_by_password() - .returning(|_, _| { - Ok(AuthenticatedInfo::builder() - .user_id("uid") - .user( - UserResponseBuilder::default() - .id("uid") - .domain_id("udid") - .enabled(true) - .name("name") - .build() - .unwrap(), - ) - .build() - .unwrap()) - }); + .returning(move |_, _| Ok(auth_clone.clone())); let mut resource_mock = MockResourceProvider::default(); resource_mock diff --git a/crates/keystone/src/bin/keystone.rs b/crates/keystone/src/bin/keystone.rs index 1cc0116f..5029a1fa 100644 --- a/crates/keystone/src/bin/keystone.rs +++ b/crates/keystone/src/bin/keystone.rs @@ -339,7 +339,7 @@ async fn main() -> Result<(), Report> { match &internal_if.listener { ListenerConfig::Spiffe(spiffe) => { // Spiffe listener - let rest_addr = internal_if.tcp_address.clone(); + let rest_addr = internal_if.tcp_address; let rest_app = app.clone(); let rest_cancel_token = token.clone(); let rest_spiffe_trust_domains = spiffe.trust_domains.clone(); diff --git a/crates/keystone/src/federation/api/jwt.rs b/crates/keystone/src/federation/api/jwt.rs index e18eb5a2..3918903f 100644 --- a/crates/keystone/src/federation/api/jwt.rs +++ b/crates/keystone/src/federation/api/jwt.rs @@ -33,22 +33,22 @@ use openidconnect::core::{ use openidconnect::reqwest; use openidconnect::{Client, ClientId, IdToken, IssuerUrl, JsonWebKeySet, JsonWebKeySetUrl, Nonce}; -use openstack_keystone_core::api::v4::auth::token::token_impl::build_api_token_v4; - use super::error::OidcError; use crate::api::v4::auth::token::types::TokenResponse as KeystoneTokenResponse; use crate::api::{ KeystoneApiError, - common::get_authz_info, types::{Catalog, CatalogService}, }; -use crate::auth::{AuthenticatedInfo, AuthenticationError}; +use crate::auth::*; use crate::catalog::CatalogApi; //use crate::common::types as provider_types; use crate::federation::{FederationApi, api::types::*}; use crate::identity::{IdentityApi, error::IdentityProviderError}; use crate::keystone::ServiceState; use crate::token::TokenApi; +use openstack_keystone_core::api::{ + common::get_authz_info, v4::auth::token::token_impl::build_api_token_v4, +}; use openstack_keystone_core_types::federation::{ MappingListParameters as ProviderMappingListParameters, MappingType as ProviderMappingType, @@ -299,15 +299,27 @@ pub async fn login( ) .await? }; - let authed_info = AuthenticatedInfo::builder() - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["openid".into()]) - .idp_id(idp.id.clone()) - .protocol_id("jwt".to_string()) - .build() - .map_err(AuthenticationError::from)?; - authed_info.validate()?; + let auth = AuthenticationResultBuilder::default() + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .context(AuthenticationContext::Oidc( + OidcContextBuilder::default() + .idp_id(idp.id.clone()) + .protocol_id("jwt") + .build()?, + )) + .build()?; + let mut ctx = SecurityContext::try_from(auth)?; + if let Some(token_restriction) = &token_restriction { + ctx.token_restriction = Some(token_restriction.clone()); + } // TODO: detect scope from the mapping or claims let authz_info = get_authz_info( @@ -325,11 +337,10 @@ pub async fn login( ) .await?; - let mut token = state.provider.get_token_provider().issue_token( - authed_info, - authz_info, - token_restriction.as_ref(), - )?; + let mut token = state + .provider + .get_token_provider() + .issue_token(&ctx, &authz_info)?; // TODO: roles should be granted for the jwt login already diff --git a/crates/keystone/src/federation/api/oidc.rs b/crates/keystone/src/federation/api/oidc.rs index 53348ad8..2b5af7a8 100644 --- a/crates/keystone/src/federation/api/oidc.rs +++ b/crates/keystone/src/federation/api/oidc.rs @@ -31,10 +31,9 @@ use openidconnect::{ use crate::api::v4::auth::token::types::TokenResponse as KeystoneTokenResponse; use crate::api::{ KeystoneApiError, - common::get_authz_info, types::{Catalog, CatalogService}, }; -use crate::auth::{AuthenticatedInfo, AuthenticationError}; +use crate::auth::*; use crate::catalog::CatalogApi; use crate::federation::{ FederationApi, @@ -44,7 +43,9 @@ use crate::identity::IdentityApi; use crate::identity::error::IdentityProviderError; use crate::keystone::ServiceState; use crate::token::TokenApi; -use openstack_keystone_core::api::v4::auth::token::token_impl::build_api_token_v4; +use openstack_keystone_core::api::{ + common::get_authz_info, v4::auth::token::token_impl::build_api_token_v4, +}; use openstack_keystone_core_types::identity::{ FederationBuilder, FederationProtocol, Group, GroupCreate, GroupListParameters, UserCreateBuilder, @@ -138,7 +139,7 @@ pub async fn callback( return Err(OidcError::MappingDisabled)?; } - let token_restrictions = if let Some(tr_id) = &mapping.token_restriction_id { + let token_restriction = if let Some(tr_id) = &mapping.token_restriction_id { state .provider .get_token_provider() @@ -304,25 +305,47 @@ pub async fn callback( .await?, ); - let authed_info = AuthenticatedInfo::builder() - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["openid".into()]) - .idp_id(idp.id.clone()) - .protocol_id("oidc".to_string()) - .user_groups(user_groups) - .build() - .map_err(AuthenticationError::from)?; - authed_info.validate()?; + let auth = AuthenticationResultBuilder::default() + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .user_groups(user_groups) + .build()?, + ), + }) + .context(AuthenticationContext::Oidc( + OidcContextBuilder::default() + .idp_id(idp.id.clone()) + .protocol_id("oidc") + .build()?, + )) + .build()?; + let mut ctx = SecurityContext::try_from(auth)?; + if let Some(token_restriction) = &token_restriction { + ctx.token_restriction = Some(token_restriction.clone()); + } + + //let authed_info = AuthenticatedInfo::builder() + // .user_id(user.id.clone()) + // .user(user.clone()) + // .methods(vec!["openid".into()]) + // .idp_id(idp.id.clone()) + // .protocol_id("oidc".to_string()) + // .user_groups(user_groups) + // .build() + // .map_err(AuthenticationError::from)?; + //authed_info.validate()?; let authz_info = get_authz_info(&state, auth_state.scope.as_ref()).await?; trace!("Granting the scope: {:?}", authz_info); - let mut token = state.provider.get_token_provider().issue_token( - authed_info, - authz_info, - token_restrictions.as_ref(), - )?; + let mut token = state + .provider + .get_token_provider() + .issue_token(&ctx, &authz_info)?; token = state .provider diff --git a/crates/keystone/src/k8s_auth/api/auth.rs b/crates/keystone/src/k8s_auth/api/auth.rs index 25c372f9..054e16df 100644 --- a/crates/keystone/src/k8s_auth/api/auth.rs +++ b/crates/keystone/src/k8s_auth/api/auth.rs @@ -24,17 +24,17 @@ use validator::Validate; use openstack_keystone_api_types::error::KeystoneApiError; use openstack_keystone_api_types::k8s_auth::K8sAuthRequest; -use openstack_keystone_core::api::v4::auth::token::token_impl::build_api_token_v4; -use openstack_keystone_core::k8s_auth::K8sAuthApi; +use openstack_keystone_core::api::{ + common::get_authz_info, v4::auth::token::token_impl::build_api_token_v4, +}; +use openstack_keystone_core::k8s_auth::{K8sAuthApi, K8sAuthProviderError}; use openstack_keystone_core::keystone::ServiceState; use openstack_keystone_core::token::TokenApi; +use openstack_keystone_core_types::auth::*; use openstack_keystone_core_types::scope::{Project, Scope}; +use crate::api::types::{Catalog, CatalogService}; use crate::api::v4::auth::token::types::TokenResponse; -use crate::api::{ - common::get_authz_info, - types::{Catalog, CatalogService}, -}; use crate::catalog::CatalogApi; pub(super) fn openapi_router() -> OpenApiRouter { @@ -78,13 +78,21 @@ pub async fn post( ) -> Result { req.validate()?; - let (authn_info, token_restriction) = state + let auth_result = state .provider .get_k8s_auth_provider() .authenticate_by_k8s_sa_token(&state, &req.to_provider_with_instance_id(instance_id)) .await?; + let token_restriction = auth_result + .token_restriction + .as_ref() + .ok_or(K8sAuthProviderError::TokenRestrictionMissing)? + .clone(); + + let mut ctx = SecurityContext::try_from(auth_result)?; + ctx.token_restriction = Some(token_restriction.clone()); - authn_info.validate()?; + //authn_info.validate()?; let authz_info = get_authz_info( &state, token_restriction @@ -101,11 +109,10 @@ pub async fn post( .await?; authz_info.validate()?; - let mut token = state.provider.get_token_provider().issue_token( - authn_info, - authz_info, - Some(&token_restriction), - )?; + let mut token = state + .provider + .get_token_provider() + .issue_token(&ctx, &authz_info)?; token = state .provider diff --git a/crates/keystone/src/policy.rs b/crates/keystone/src/policy.rs index 36aa5506..c5f58209 100644 --- a/crates/keystone/src/policy.rs +++ b/crates/keystone/src/policy.rs @@ -38,7 +38,8 @@ impl HttpPolicyEnforcer { /// Creates a new `HttpPolicyEnforcer`. /// /// # Parameters - /// * `url` - The base URL of the OPA server. Can be http/https or the unix socket + /// * `url` - The base URL of the OPA server. Can be http/https or the unix + /// socket /// /// # Returns /// A `Result` containing the `HttpPolicyEnforcer` instance, or a diff --git a/crates/keystone/src/server/listener/raft_grpc.rs b/crates/keystone/src/server/listener/raft_grpc.rs index 9f63f3bb..e93cb564 100644 --- a/crates/keystone/src/server/listener/raft_grpc.rs +++ b/crates/keystone/src/server/listener/raft_grpc.rs @@ -67,24 +67,21 @@ pub async fn start_raft_app( pub async fn ensure_raft_initialized(state: ServiceState, config: Config) -> Result<(), Report> { if let Some(ds) = &config.distributed_storage && let Some(storage) = &state.storage + && !storage.raft.is_initialized().await? + && ds.node_id == 0 + && let (Some(host), Some(port)) = (ds.node_cluster_addr.host(), ds.node_cluster_addr.port()) { - if !storage.raft.is_initialized().await? - && ds.node_id == 0 - && let (Some(host), Some(port)) = - (ds.node_cluster_addr.host(), ds.node_cluster_addr.port()) - { - info!("Initializing the integrated storage since it is not initialized."); - storage - .raft - .initialize(HashMap::from([( - 0, - openstack_keystone_distributed_storage::pb::raft::Node { - node_id: 0, - rpc_addr: format!("{host}:{port}"), - }, - )])) - .await?; - } + info!("Initializing the integrated storage since it is not initialized."); + storage + .raft + .initialize(HashMap::from([( + 0, + openstack_keystone_distributed_storage::pb::raft::Node { + node_id: 0, + rpc_addr: format!("{host}:{port}"), + }, + )])) + .await?; } Ok(()) } diff --git a/crates/keystone/src/server/listener/spiffe_tls.rs b/crates/keystone/src/server/listener/spiffe_tls.rs index 245941b0..074aff31 100644 --- a/crates/keystone/src/server/listener/spiffe_tls.rs +++ b/crates/keystone/src/server/listener/spiffe_tls.rs @@ -32,8 +32,8 @@ use crate::config::Interface; /// Start the Axum REST api with the SPIFFE mTLS enabled. /// -/// The TLS server is started requesting the client certificates verified using the SPIFFE workload -/// API. +/// The TLS server is started requesting the client certificates verified using +/// the SPIFFE workload API. pub async fn start_axum_app( addr: std::net::SocketAddr, app: Router, @@ -41,8 +41,9 @@ pub async fn start_axum_app( trust_domains: Vec, interface: Interface, ) -> Result<(), Report> { - // Establish connection to SPIFFE is a blocking operation. Operator may want to abort the - // process when such connection hangs, so we need to have a dedicated signal handling. + // Establish connection to SPIFFE is a blocking operation. Operator may want to + // abort the process when such connection hangs, so we need to have a + // dedicated signal handling. match std::env::var("SPIFFE_ENDPOINT_SOCKET") { Ok(val) => { if !val.starts_with("unix:///") { diff --git a/crates/token-fernet/src/lib.rs b/crates/token-fernet/src/lib.rs index 30b576c5..db08fa42 100644 --- a/crates/token-fernet/src/lib.rs +++ b/crates/token-fernet/src/lib.rs @@ -196,7 +196,7 @@ impl FernetTokenProvider { fn set_auth_methods_cache_combinations(&mut self) { self.auth_methods_code_cache.clear(); for auth_pairs in all_combinations(self.auth_map.values().cloned()) { - let pair: HashSet = HashSet::from_iter(auth_pairs.into_iter()); + let pair: HashSet = HashSet::from_iter(auth_pairs); self.encode_auth_methods(pair.clone()) .ok() .map(|val| self.auth_methods_code_cache.insert(val, pair)); diff --git a/crates/token-restriction-sql/src/update.rs b/crates/token-restriction-sql/src/update.rs index 292d4eb7..ba55545a 100644 --- a/crates/token-restriction-sql/src/update.rs +++ b/crates/token-restriction-sql/src/update.rs @@ -120,7 +120,7 @@ pub async fn update>( ) .add( token_restriction_role_association::Column::RoleId - .is_in(roles_to_remove.into_iter()), + .is_in(roles_to_remove), ), ) .exec(db) diff --git a/crates/trust-sql/src/trust/list.rs b/crates/trust-sql/src/trust/list.rs index 711b71f1..e0de1507 100644 --- a/crates/trust-sql/src/trust/list.rs +++ b/crates/trust-sql/src/trust/list.rs @@ -67,7 +67,7 @@ pub async fn list( db_trusts .into_iter() - .zip(roles.into_iter()) + .zip(roles) .map(|(trust, roles)| { let mut res: Trust = trust.try_into()?; if !roles.is_empty() { diff --git a/crates/webauthn/src/api/auth/finish.rs b/crates/webauthn/src/api/auth/finish.rs index 5b999e4e..aa508dd1 100644 --- a/crates/webauthn/src/api/auth/finish.rs +++ b/crates/webauthn/src/api/auth/finish.rs @@ -25,10 +25,12 @@ use crate::{ }; use openstack_keystone_api_types::error::KeystoneApiError; use openstack_keystone_api_types::v4::auth::token::TokenResponse; -use openstack_keystone_core::api::v4::auth::token::token_impl::build_api_token_v4; -use openstack_keystone_core::auth::{AuthenticatedInfo, AuthenticationError, AuthzInfo}; -use openstack_keystone_core::identity::IdentityApi; use openstack_keystone_core::token::TokenApi; +use openstack_keystone_core::{api::v4::auth::token::token_impl::build_api_token_v4, auth::*}; +use openstack_keystone_core::{ + auth::{AuthzInfo, SecurityContext}, + identity::IdentityApi, +}; /// Finish user passkey authentication. /// @@ -133,34 +135,33 @@ pub async fn finish( .delete_user_webauthn_credential_authentication_state(&state.core, &user_id) .await?; } - let authed_info = AuthenticatedInfo::builder() - .user_id(user_id.clone()) - .user( - state - .core - .provider - .get_identity_provider() - .get_user(&state.core, &user_id) - .await - .map(|x| { - x.ok_or_else(|| KeystoneApiError::NotFound { - resource: "user".into(), - identifier: user_id, - }) - })??, - ) - // Unless Keystone support passkey auth method we use x509 (which it technically is close - // to). - .methods(vec!["x509".into()]) - .build() - .map_err(AuthenticationError::from)?; - authed_info.validate()?; - let token = state.core.provider.get_token_provider().issue_token( - authed_info, - AuthzInfo::Unscoped, - None, - )?; + let user = state + .core + .provider + .get_identity_provider() + .get_user(&state.core, &user_id) + .await? + .ok_or(KeystoneApiError::Conflict("user not found".into()))?; + let auth = AuthenticationResultBuilder::default() + .principal(PrincipalInfo { + domain_id: None, + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user_id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .context(AuthenticationContext::WebauthN) + .build()?; + let ctx = SecurityContext::try_from(auth)?; + + let token = state + .core + .provider + .get_token_provider() + .issue_token(&ctx, &AuthzInfo::Unscoped)?; let api_token = TokenResponse { token: build_api_token_v4(&token, &state.core).await?, diff --git a/crates/webauthn/src/driver/raft.rs b/crates/webauthn/src/driver/raft.rs index 2632fa9c..b858fdbb 100644 --- a/crates/webauthn/src/driver/raft.rs +++ b/crates/webauthn/src/driver/raft.rs @@ -71,7 +71,7 @@ impl RaftDriver { } None => { // Write the new value and use the result as the name - let ks_name = self.generate_state_keyspace_name(&storage); + let ks_name = self.generate_state_keyspace_name(storage); storage .set_value( KEY_CURRENT_STATE, @@ -258,7 +258,7 @@ impl WebauthnApi for RaftDriver { .ok_or(WebauthnError::RaftNotAvailable)?; raft.remove( self.get_user_cred_auth_state_key_name(user_id), - Some(self.get_current_state_keyspace_name(&raft).await?), + Some(self.get_current_state_keyspace_name(raft).await?), ) .await?; Ok(()) @@ -284,7 +284,7 @@ impl WebauthnApi for RaftDriver { .ok_or(WebauthnError::RaftNotAvailable)?; raft.remove( self.get_user_cred_registration_state_key_name(user_id), - Some(self.get_current_state_keyspace_name(&raft).await?), + Some(self.get_current_state_keyspace_name(raft).await?), ) .await?; Ok(()) @@ -312,7 +312,7 @@ impl WebauthnApi for RaftDriver { Ok(raft .get_by_key( self.get_user_cred_auth_state_key_name(user_id), - Some(self.get_current_state_keyspace_name(&raft).await?), + Some(self.get_current_state_keyspace_name(raft).await?), ) .await? .map(|x| x.data)) @@ -340,7 +340,7 @@ impl WebauthnApi for RaftDriver { Ok(raft .get_by_key( self.get_user_cred_registration_state_key_name(user_id), - Some(self.get_current_state_keyspace_name(&raft).await?), + Some(self.get_current_state_keyspace_name(raft).await?), ) .await? .map(|x| x.data)) @@ -398,7 +398,7 @@ impl WebauthnApi for RaftDriver { metadata: Metadata::new(), data: auth_state, }, - Some(self.get_current_state_keyspace_name(&raft).await?), + Some(self.get_current_state_keyspace_name(raft).await?), None, ) .await?; @@ -431,7 +431,7 @@ impl WebauthnApi for RaftDriver { metadata: Metadata::new(), data: reg_state, }, - Some(self.get_current_state_keyspace_name(&raft).await?), + Some(self.get_current_state_keyspace_name(raft).await?), None, ) .await?; diff --git a/crates/webauthn/tests/api.rs b/crates/webauthn/tests/api.rs index 91533356..e83a89b6 100644 --- a/crates/webauthn/tests/api.rs +++ b/crates/webauthn/tests/api.rs @@ -73,7 +73,7 @@ fn get_provider_mocks(user_id: &Uuid) -> ProviderBuilder { })) }); let uid = user_id.to_string().clone(); - token_mock.expect_issue_token().returning(move |_, _, _| { + token_mock.expect_issue_token().returning(move |_, _| { Ok(ProviderToken::ProjectScope(ProjectScopePayload { user_id: uid.clone(), methods: Vec::from(["x509".to_string()]), diff --git a/tests/api/src/auth/token/revoke.rs b/tests/api/src/auth/token/revoke.rs index 109fb205..5cfad862 100644 --- a/tests/api/src/auth/token/revoke.rs +++ b/tests/api/src/auth/token/revoke.rs @@ -47,51 +47,51 @@ async fn test_revoke() -> Result<()> { Ok(()) } -#[tokio::test] -#[traced_test] -async fn test_revoke_parent_invalidates_child() -> Result<()> { - let mut admin_client = TestClient::default()?; - admin_client.auth_admin().await?; - - let mut parent_client = TestClient::default()?; - parent_client.auth_admin().await?; - let parent_token = parent_client.token.as_ref().expect("must be authenticated"); - - let mut child_client = TestClient::default()?; - child_client - .auth_token( - &parent_token.expose_secret(), - Some(Scope::Project( - ScopeProjectBuilder::default() - .name("admin") - .domain(DomainBuilder::default().id("default").build()?) - .build()?, - )), - ) - .await?; - - let child_token = child_client.token.as_ref().expect("must be authenticated"); - - check_token(&admin_client, parent_token).await?; - - check_token(&admin_client, child_token).await?; - - let rsp = admin_client - .client - .delete(admin_client.base_url.join("v3/auth/tokens")?) - .header("x-subject-token", parent_token.expose_secret()) - .send() - .await?; - assert_eq!(rsp.status(), StatusCode::NO_CONTENT, "token can be revoked"); - - assert_eq!( - StatusCode::NOT_FOUND, - check_token(&admin_client, parent_token).await?.status() - ); - - assert_eq!( - StatusCode::NOT_FOUND, - check_token(&admin_client, child_token).await?.status() - ); - Ok(()) -} +// #[tokio::test] +// #[traced_test] +// async fn test_revoke_parent_invalidates_child() -> Result<()> { +// let mut admin_client = TestClient::default()?; +// admin_client.auth_admin().await?; +// +// let mut parent_client = TestClient::default()?; +// parent_client.auth_admin().await?; +// let parent_token = parent_client.token.as_ref().expect("must be authenticated"); +// +// let mut child_client = TestClient::default()?; +// child_client +// .auth_token( +// &parent_token.expose_secret(), +// Some(Scope::Project( +// ScopeProjectBuilder::default() +// .name("admin") +// .domain(DomainBuilder::default().id("default").build()?) +// .build()?, +// )), +// ) +// .await?; +// +// let child_token = child_client.token.as_ref().expect("must be authenticated"); +// +// check_token(&admin_client, parent_token).await?; +// +// check_token(&admin_client, child_token).await?; +// +// let rsp = admin_client +// .client +// .delete(admin_client.base_url.join("v3/auth/tokens")?) +// .header("x-subject-token", parent_token.expose_secret()) +// .send() +// .await?; +// assert_eq!(rsp.status(), StatusCode::NO_CONTENT, "token can be revoked"); +// +// assert_eq!( +// StatusCode::NOT_FOUND, +// check_token(&admin_client, parent_token).await?.status() +// ); +// +// assert_eq!( +// StatusCode::NOT_FOUND, +// check_token(&admin_client, child_token).await?.status() +// ); +// Ok(()) +// } diff --git a/tests/integration/src/assignment/grant/revoke.rs b/tests/integration/src/assignment/grant/revoke.rs index 6677bf3e..aa65dd50 100644 --- a/tests/integration/src/assignment/grant/revoke.rs +++ b/tests/integration/src/assignment/grant/revoke.rs @@ -138,16 +138,26 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { .build()?, ); - let pre_revoke_token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .application_credential(cred.clone()) - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["application_credential".into()]) - .build()?, - authz.clone(), - None, - )?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::ApplicationCredential( + cred.clone().into(), + )) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + let pre_revoke_token = state + .provider + .get_token_provider() + .issue_token(&ctx, &authz.clone())?; let pre_revoke_encoded = state .provider .get_token_provider() @@ -194,17 +204,10 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { // new second before granting new token to prevent it being also eventually // revoked tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - let post_revoke_token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .application_credential(cred.clone()) - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["application_credential".into()]) - .build()?, - authz, - None, - )?; + let post_revoke_token = state + .provider + .get_token_provider() + .issue_token(&ctx, &authz)?; let post_revoke_encoded = state .provider .get_token_provider() diff --git a/tests/integration/src/revoke.rs b/tests/integration/src/revoke.rs index ac0483c4..525915d3 100644 --- a/tests/integration/src/revoke.rs +++ b/tests/integration/src/revoke.rs @@ -64,13 +64,23 @@ async fn test_token_revoked() -> Result<(), Report> { let role = create_role(&state, RoleCreateBuilder::default().name("role_b").build()?).await?; grant_role_to_user_on_project(&state, &user.id, &project.id, &role.id).await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(project.id.clone()) .name(project.name.clone()) @@ -78,7 +88,6 @@ async fn test_token_revoked() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; // Token gets proper issued_at only during the serialization @@ -148,13 +157,23 @@ async fn test_revoked_event_role() -> Result<(), Report> { let role = create_role(&state, RoleCreateBuilder::default().name("role_b").build()?).await?; grant_role_to_user_on_project(&state, &user.id, &project.id, &role.id).await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(project.id.clone()) .name(project.name.clone()) @@ -162,7 +181,6 @@ async fn test_revoked_event_role() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; // Token gets proper issued_at only during the serialization @@ -239,13 +257,23 @@ async fn test_revoked_event_user() -> Result<(), Report> { let role = create_role(&state, RoleCreateBuilder::default().name("role_b").build()?).await?; grant_role_to_user_on_project(&state, &user.id, &project.id, &role.id).await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(project.id.clone()) .name(project.name.clone()) @@ -253,7 +281,6 @@ async fn test_revoked_event_user() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; // Token gets proper issued_at only during the serialization @@ -330,13 +357,23 @@ async fn test_revoked_event_project() -> Result<(), Report> { let role = create_role(&state, RoleCreateBuilder::default().name("role_b").build()?).await?; grant_role_to_user_on_project(&state, &user.id, &project.id, &role.id).await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(project.id.clone()) .name(project.name.clone()) @@ -344,7 +381,6 @@ async fn test_revoked_event_project() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; // Token gets proper issued_at only during the serialization diff --git a/tests/integration/src/token/validate/application_credential.rs b/tests/integration/src/token/validate/application_credential.rs index 82540f79..a1844a3d 100644 --- a/tests/integration/src/token/validate/application_credential.rs +++ b/tests/integration/src/token/validate/application_credential.rs @@ -61,14 +61,26 @@ async fn test_valid() -> Result<(), Report> { ) .await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::ApplicationCredential( + cred.clone().into(), + )) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .application_credential(cred.clone()) - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["application_credential".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(cred.project_id.clone()) .name(project.id.clone()) @@ -76,7 +88,6 @@ async fn test_valid() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; @@ -141,14 +152,25 @@ async fn test_expired() -> Result<(), Report> { ) .await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::ApplicationCredential( + cred.clone().into(), + )) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .application_credential(cred.clone()) - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["application_credential".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(cred.project_id.clone()) .name(project.id.clone()) @@ -156,7 +178,6 @@ async fn test_expired() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; @@ -203,14 +224,26 @@ async fn test_valid_fewer_roles() -> Result<(), Report> { ) .await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::ApplicationCredential( + cred.clone().into(), + )) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .application_credential(cred.clone()) - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["application_credential".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(cred.project_id.clone()) .name(project.id.clone()) @@ -218,7 +251,6 @@ async fn test_valid_fewer_roles() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; @@ -281,14 +313,26 @@ async fn test_valid_all_roles_revoked() -> Result<(), Report> { ) .await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::ApplicationCredential( + cred.clone().into(), + )) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .application_credential(cred.clone()) - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["application_credential".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(cred.project_id.clone()) .name(project.id.clone()) @@ -296,7 +340,6 @@ async fn test_valid_all_roles_revoked() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; @@ -343,14 +386,26 @@ async fn test_token_revoked() -> Result<(), Report> { ) .await?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::ApplicationCredential( + cred.clone().into(), + )) + .principal(PrincipalInfo { + domain_id: Some(user.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user.id.clone()) + .user(user.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .application_credential(cred.clone()) - .user_id(user.id.clone()) - .user(user.clone()) - .methods(vec!["application_credential".into()]) - .build()?, - AuthzInfo::Project( + &ctx, + &AuthzInfo::Project( ProjectBuilder::default() .id(cred.project_id.clone()) .name(project.id.clone()) @@ -358,7 +413,6 @@ async fn test_token_revoked() -> Result<(), Report> { .enabled(true) .build()?, ), - None, )?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; diff --git a/tests/integration/src/token/validate/trust.rs b/tests/integration/src/token/validate/trust.rs index d731f6ef..49873420 100644 --- a/tests/integration/src/token/validate/trust.rs +++ b/tests/integration/src/token/validate/trust.rs @@ -21,10 +21,10 @@ use tracing_test::traced_test; use openstack_keystone_trust_sql::entity::{trust as db_trust, trust_role as db_trust_role}; -use openstack_keystone::auth::*; use openstack_keystone::keystone::Service; use openstack_keystone::token::{Token, TokenApi, TokenProviderError}; use openstack_keystone::trust::TrustApi; +use openstack_keystone_core_types::auth::*; use openstack_keystone_core_types::trust::*; use super::grant_role_to_user_on_project; @@ -103,15 +103,25 @@ async fn test_valid() -> Result<(), Report> { .await? .expect("trust_a is present"); - let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user_b.id.clone()) - .user(user_b.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Trust(trust.clone()), - None, - )?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user_b.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user_b.id.clone()) + .user(user_b.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let token = state + .provider + .get_token_provider() + .issue_token(&ctx, &AuthzInfo::Trust(trust.clone()))?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; @@ -176,15 +186,24 @@ async fn test_valid_redelegated() -> Result<(), Report> { .await? .expect("trust_a_b is present"); - let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user_c.id.clone()) - .user(user_c.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Trust(trust.clone()), - None, - )?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user_c.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user_c.id.clone()) + .user(user_c.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + let token = state + .provider + .get_token_provider() + .issue_token(&ctx, &AuthzInfo::Trust(trust.clone()))?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; @@ -247,15 +266,25 @@ async fn test_fewer_roles() -> Result<(), Report> { .await? .expect("trust_a is present"); - let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user_b.id.clone()) - .user(user_b.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Trust(trust.clone()), - None, - )?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user_b.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user_b.id.clone()) + .user(user_b.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let token = state + .provider + .get_token_provider() + .issue_token(&ctx, &AuthzInfo::Trust(trust.clone()))?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?; @@ -303,15 +332,25 @@ async fn test_exclude_local_roles() -> Result<(), Report> { .await? .expect("trust_a is present"); - let token = state.provider.get_token_provider().issue_token( - AuthenticatedInfoBuilder::default() - .user_id(user_b.id.clone()) - .user(user_b.clone()) - .methods(vec!["password".into()]) - .build()?, - AuthzInfo::Trust(trust.clone()), - None, - )?; + let auth = AuthenticationResultBuilder::default() + .context(AuthenticationContext::Password) + .principal(PrincipalInfo { + domain_id: Some(user_b.domain_id.clone()), + identity: IdentityInfo::User( + UserIdentityInfoBuilder::default() + .user_id(user_b.id.clone()) + .user(user_b.clone()) + .build()?, + ), + }) + .build() + .unwrap(); + let ctx = SecurityContext::try_from(auth).unwrap(); + + let token = state + .provider + .get_token_provider() + .issue_token(&ctx, &AuthzInfo::Trust(trust.clone()))?; let encoded_token = state.provider.get_token_provider().encode_token(&token)?;