From 1691fda4c7c67222f545e2c5248b1d6a3566be33 Mon Sep 17 00:00:00 2001 From: gtema Date: Mon, 11 May 2026 18:46:33 +0200 Subject: [PATCH] feat: Introduce SecurityContext Introduce the SecurityContext instead of the AuthenticatedInfo that is more flexible and allow being used for the SPIFFE issued principals while also trying to make heavy use of enums to prevent undefined states while re-authenticating. Additionally this prevents many logic errors my having a precise state of the authentication and authorization by heavy use of enums. --- Cargo.lock | 7 + crates/api-types/src/error_conv.rs | 9 + .../src/application_credential/create.rs | 2 +- .../src/application_credential/list.rs | 5 +- crates/cli-manage/src/storage/demote.rs | 2 +- crates/cli-manage/src/storage/promote.rs | 2 +- crates/cli-manage/src/storage/remove_peer.rs | 2 +- crates/config/src/lib.rs | 4 +- crates/core-types/Cargo.toml | 2 +- crates/core-types/src/auth.rs | 958 +++++++++++++++++- crates/core-types/src/k8s_auth/error.rs | 4 + crates/core-types/src/lib.rs | 1 + .../core-types/src/revoke/revocation_event.rs | 2 +- crates/core-types/src/token.rs | 90 ++ crates/core-types/src/token/error.rs | 4 + .../token/payload/application_credential.rs | 77 ++ .../src/token/payload/domain_scoped.rs | 64 ++ .../token/payload/federation_domain_scoped.rs | 104 ++ .../payload/federation_project_scoped.rs | 102 ++ .../src/token/payload/federation_unscoped.rs | 85 ++ .../src/token/payload/project_scoped.rs | 64 ++ .../src/token/payload/restricted.rs | 96 +- .../src/token/payload/system_scoped.rs | 55 + crates/core-types/src/token/payload/trust.rs | 67 ++ .../core-types/src/token/payload/unscoped.rs | 63 ++ crates/core/src/identity/backend.rs | 4 +- crates/core/src/identity/mock.rs | 4 +- crates/core/src/identity/mod.rs | 4 +- crates/core/src/identity/provider_api.rs | 4 +- crates/core/src/identity/service.rs | 4 +- crates/core/src/k8s_auth/auth.rs | 64 +- crates/core/src/k8s_auth/mock.rs | 5 +- crates/core/src/k8s_auth/mod.rs | 5 +- crates/core/src/k8s_auth/provider_api.rs | 5 +- crates/core/src/k8s_auth/service.rs | 5 +- crates/core/src/provider.rs | 24 +- crates/core/src/token/mock.rs | 9 +- crates/core/src/token/mod.rs | 19 +- crates/core/src/token/provider_api.rs | 11 +- crates/core/src/token/service.rs | 825 ++------------- crates/identity-sql/src/authenticate.rs | 29 +- crates/identity-sql/src/lib.rs | 4 +- crates/k8s-auth-raft/src/lib.rs | 14 +- crates/keystone/Cargo.toml | 2 +- crates/keystone/src/api/common.rs | 99 +- .../keystone/src/api/v3/auth/token/common.rs | 141 ++- .../keystone/src/api/v3/auth/token/create.rs | 96 +- crates/keystone/src/bin/keystone.rs | 2 +- crates/keystone/src/federation/api/jwt.rs | 47 +- crates/keystone/src/federation/api/oidc.rs | 61 +- crates/keystone/src/k8s_auth/api/auth.rs | 33 +- crates/keystone/src/policy.rs | 3 +- .../keystone/src/server/listener/raft_grpc.rs | 31 +- .../src/server/listener/spiffe_tls.rs | 9 +- crates/token-fernet/src/lib.rs | 2 +- crates/token-restriction-sql/src/update.rs | 2 +- crates/trust-sql/src/trust/list.rs | 2 +- crates/webauthn/src/api/auth/finish.rs | 61 +- crates/webauthn/src/driver/raft.rs | 14 +- crates/webauthn/tests/api.rs | 2 +- tests/api/src/auth/token/revoke.rs | 96 +- .../src/assignment/grant/revoke.rs | 45 +- tests/integration/src/revoke.rs | 92 +- .../token/validate/application_credential.rs | 134 ++- tests/integration/src/token/validate/trust.rs | 113 ++- 65 files changed, 2626 insertions(+), 1371 deletions(-) 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)?;