From c1c9cb7ae4b45376de7c433425ebfc10fef1660e Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Thu, 13 Nov 2025 18:37:11 +0000 Subject: [PATCH] chore: Reorganize the token provider Apply a newer provider structure to keep modules smaller and better structured. --- .github/workflows/benchmark_fork_run.yml | 2 +- benches/fernet_token.rs | 4 +- src/token/backend.rs | 36 +++ src/token/{ => backend}/fernet.rs | 20 +- .../fernet}/application_credential.rs | 85 ++----- src/token/backend/fernet/restricted.rs | 111 ++++++++++ .../fernet/utils.rs} | 0 src/token/error.rs | 15 +- src/token/mock.rs | 112 ++++++++++ src/token/mod.rs | 207 +----------------- src/token/restricted.rs | 180 --------------- src/token/types.rs | 127 +++-------- src/token/types/application_credential.rs | 74 +++++++ src/token/{ => types}/domain_scoped.rs | 21 +- .../{ => types}/federation_domain_scoped.rs | 33 ++- .../{ => types}/federation_project_scoped.rs | 33 ++- src/token/{ => types}/federation_unscoped.rs | 29 ++- src/token/{ => types}/project_scoped.rs | 21 +- src/token/types/provider_api.rs | 107 +++++++++ src/token/types/restricted.rs | 152 +++++++++++++ src/token/{ => types}/unscoped.rs | 17 +- 21 files changed, 748 insertions(+), 638 deletions(-) create mode 100644 src/token/backend.rs rename src/token/{ => backend}/fernet.rs (98%) rename src/token/{ => backend/fernet}/application_credential.rs (53%) create mode 100644 src/token/backend/fernet/restricted.rs rename src/token/{fernet_utils.rs => backend/fernet/utils.rs} (100%) create mode 100644 src/token/mock.rs delete mode 100644 src/token/restricted.rs create mode 100644 src/token/types/application_credential.rs rename src/token/{ => types}/domain_scoped.rs (87%) rename src/token/{ => types}/federation_domain_scoped.rs (82%) rename src/token/{ => types}/federation_project_scoped.rs (82%) rename src/token/{ => types}/federation_unscoped.rs (83%) rename src/token/{ => types}/project_scoped.rs (87%) create mode 100644 src/token/types/provider_api.rs create mode 100644 src/token/types/restricted.rs rename src/token/{ => types}/unscoped.rs (88%) diff --git a/.github/workflows/benchmark_fork_run.yml b/.github/workflows/benchmark_fork_run.yml index eb8b70bc..6b369539 100644 --- a/.github/workflows/benchmark_fork_run.yml +++ b/.github/workflows/benchmark_fork_run.yml @@ -30,7 +30,7 @@ jobs: - name: Track base branch benchmarks with Bencher run: | - cargo bench > benchmark_results.log + cargo bench --features bench_internals > benchmark_results.log - name: Upload Benchmark Results uses: actions/upload-artifact@v5 diff --git a/benches/fernet_token.rs b/benches/fernet_token.rs index 3eaa40b9..3547ef15 100644 --- a/benches/fernet_token.rs +++ b/benches/fernet_token.rs @@ -7,8 +7,8 @@ use std::io::Write; use tempfile::tempdir; use openstack_keystone::config::Config; -use openstack_keystone::token::fernet::FernetTokenProvider; -use openstack_keystone::token::fernet::bench_get_fernet_timestamp; +use openstack_keystone::token::backend::fernet::FernetTokenProvider; +use openstack_keystone::token::backend::fernet::bench_get_fernet_timestamp; //use openstack_keystone::token::types::TokenBackend; fn decode(backend: &FernetTokenProvider, token: &str) { diff --git a/src/token/backend.rs b/src/token/backend.rs new file mode 100644 index 00000000..da1c5147 --- /dev/null +++ b/src/token/backend.rs @@ -0,0 +1,36 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//! Token provider backends. + +use dyn_clone::DynClone; + +use crate::config::Config; +use crate::token::{TokenProviderError, types::Token}; + +pub mod fernet; +pub use fernet::*; + +/// Token Provider backend interface. +pub trait TokenBackend: DynClone + Send + Sync + std::fmt::Debug { + /// Set config. + fn set_config(&mut self, g: Config); + + /// Extract the token from string. + fn decode(&self, credential: &str) -> Result; + + /// Extract the token from string. + fn encode(&self, token: &Token) -> Result; +} + +dyn_clone::clone_trait_object!(TokenBackend); diff --git a/src/token/fernet.rs b/src/token/backend/fernet.rs similarity index 98% rename from src/token/fernet.rs rename to src/token/backend/fernet.rs index 8334922d..4d5d8bd3 100644 --- a/src/token/fernet.rs +++ b/src/token/backend/fernet.rs @@ -30,14 +30,22 @@ use std::io::{Cursor, Write}; use tracing::trace; use crate::config::Config; +use crate::token::backend::TokenBackend; use crate::token::{ - TokenProviderError, application_credential::ApplicationCredentialPayload, - domain_scoped::DomainScopePayload, federation_domain_scoped::FederationDomainScopePayload, - federation_project_scoped::FederationProjectScopePayload, - federation_unscoped::FederationUnscopedPayload, fernet_utils::FernetUtils, - project_scoped::ProjectScopePayload, restricted::RestrictedPayload, types::*, - unscoped::UnscopedPayload, + TokenProviderError, + types::{ + application_credential::ApplicationCredentialPayload, domain_scoped::DomainScopePayload, + federation_domain_scoped::FederationDomainScopePayload, + federation_project_scoped::FederationProjectScopePayload, + federation_unscoped::FederationUnscopedPayload, project_scoped::ProjectScopePayload, + restricted::RestrictedPayload, unscoped::UnscopedPayload, *, + }, }; +use utils::FernetUtils; + +mod application_credential; +mod restricted; +pub mod utils; #[derive(Clone)] pub struct FernetTokenProvider { diff --git a/src/token/application_credential.rs b/src/token/backend/fernet/application_credential.rs similarity index 53% rename from src/token/application_credential.rs rename to src/token/backend/fernet/application_credential.rs index 5263ed5c..333cc3fb 100644 --- a/src/token/application_credential.rs +++ b/src/token/backend/fernet/application_credential.rs @@ -12,74 +12,15 @@ // // SPDX-License-Identifier: Apache-2.0 -use chrono::{DateTime, Utc}; -use derive_builder::Builder; use rmp::{decode::read_pfix, encode::write_pfix}; -use serde::Serialize; use std::io::Write; -use crate::assignment::types::Role; -use crate::identity::types::UserResponse; -use crate::resource::types::Project; use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, - types::Token, + types::ApplicationCredentialPayload, }; -#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] -#[builder(setter(into))] -pub struct ApplicationCredentialPayload { - pub user_id: String, - #[builder(default, setter(name = _methods))] - pub methods: Vec, - #[builder(default, setter(name = _audit_ids))] - pub audit_ids: Vec, - pub expires_at: DateTime, - pub project_id: String, - pub application_credential_id: String, - - #[builder(default)] - pub issued_at: DateTime, - #[builder(default)] - pub user: Option, - #[builder(default)] - pub roles: Vec, - #[builder(default)] - pub project: Option, -} - -impl ApplicationCredentialPayloadBuilder { - pub fn methods(&mut self, iter: I) -> &mut Self - where - I: Iterator, - V: Into, - { - self.methods - .get_or_insert_with(Vec::new) - .extend(iter.map(Into::into)); - self - } - - pub fn audit_ids(&mut self, iter: I) -> &mut Self - where - I: Iterator, - V: Into, - { - self.audit_ids - .get_or_insert_with(Vec::new) - .extend(iter.map(Into::into)); - self - } -} - -impl From for Token { - fn from(value: ApplicationCredentialPayload) -> Self { - Self::ApplicationCredential(value) - } -} - impl MsgPackToken for ApplicationCredentialPayload { type Token = Self; @@ -88,16 +29,16 @@ impl MsgPackToken for ApplicationCredentialPayload { wd: &mut W, fernet_provider: &FernetTokenProvider, ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; + utils::write_uuid(wd, &self.user_id)?; write_pfix( wd, fernet_provider.encode_auth_methods(self.methods.clone())?, ) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_uuid(wd, &self.project_id)?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; - fernet_utils::write_uuid(wd, &self.application_credential_id)?; + utils::write_uuid(wd, &self.project_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_uuid(wd, &self.application_credential_id)?; Ok(()) } @@ -107,15 +48,15 @@ impl MsgPackToken for ApplicationCredentialPayload { fernet_provider: &FernetTokenProvider, ) -> Result { // Order of reading is important - let user_id = fernet_utils::read_uuid(rd)?; + let user_id = utils::read_uuid(rd)?; let methods: Vec = fernet_provider .decode_auth_methods(read_pfix(rd)?)? .into_iter() .collect(); - let project_id = fernet_utils::read_uuid(rd)?; - let expires_at = fernet_utils::read_time(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); - let application_credential_id = fernet_utils::read_uuid(rd)?; + let project_id = utils::read_uuid(rd)?; + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); + let application_credential_id = utils::read_uuid(rd)?; Ok(Self { user_id, @@ -134,8 +75,8 @@ mod tests { use chrono::{Local, SubsecRound}; use uuid::Uuid; - use super::super::tests::setup_config; use super::*; + use crate::token::tests::setup_config; #[test] fn test_roundtrip() { diff --git a/src/token/backend/fernet/restricted.rs b/src/token/backend/fernet/restricted.rs new file mode 100644 index 00000000..6ccb44c7 --- /dev/null +++ b/src/token/backend/fernet/restricted.rs @@ -0,0 +1,111 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//! Restricted token Fernet implementation. + +use rmp::{decode::read_pfix, encode::write_pfix}; +use std::io::Write; + +use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, + error::TokenProviderError, + types::RestrictedPayload, +}; + +impl MsgPackToken for RestrictedPayload { + type Token = Self; + + fn assemble( + &self, + wd: &mut W, + fernet_provider: &FernetTokenProvider, + ) -> Result<(), TokenProviderError> { + utils::write_uuid(wd, &self.user_id)?; + write_pfix( + wd, + fernet_provider.encode_auth_methods(self.methods.clone())?, + ) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + utils::write_uuid(wd, &self.token_restriction_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_uuid(wd, &self.project_id)?; + utils::write_bool(wd, self.allow_renew)?; + utils::write_bool(wd, self.allow_rescope)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; + + Ok(()) + } + + fn disassemble( + rd: &mut &[u8], + fernet_provider: &FernetTokenProvider, + ) -> Result { + // Order of reading is important + let user_id = utils::read_uuid(rd)?; + let methods: Vec = fernet_provider + .decode_auth_methods(read_pfix(rd)?)? + .into_iter() + .collect(); + let token_restriction_id = utils::read_uuid(rd)?; + let expires_at = utils::read_time(rd)?; + let project_id = utils::read_uuid(rd)?; + let allow_renew = utils::read_bool(rd)?; + let allow_rescope = utils::read_bool(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); + Ok(Self { + user_id, + methods, + expires_at, + audit_ids, + token_restriction_id, + project_id, + allow_renew, + allow_rescope, + + ..Default::default() + }) + } +} + +#[cfg(test)] +mod tests { + use chrono::{Local, SubsecRound}; + use uuid::Uuid; + + use super::*; + use crate::token::tests::setup_config; + + #[test] + fn test_roundtrip() { + let token = RestrictedPayload { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["openid".into()], + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + token_restriction_id: "trid".into(), + project_id: "pid".into(), + allow_renew: true, + allow_rescope: true, + ..Default::default() + }; + + let provider = FernetTokenProvider::new(setup_config()); + + let mut buf = vec![]; + token.assemble(&mut buf, &provider).unwrap(); + let encoded_buf = buf.clone(); + let decoded = + RestrictedPayload::disassemble(&mut encoded_buf.as_slice(), &provider).unwrap(); + assert_eq!(token, decoded); + } +} diff --git a/src/token/fernet_utils.rs b/src/token/backend/fernet/utils.rs similarity index 100% rename from src/token/fernet_utils.rs rename to src/token/backend/fernet/utils.rs diff --git a/src/token/error.rs b/src/token/error.rs index d3999649..c1c69466 100644 --- a/src/token/error.rs +++ b/src/token/error.rs @@ -11,6 +11,7 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 +//! Token provider errors. use sea_orm::SqlErr; use std::num::TryFromIntError; @@ -119,49 +120,49 @@ pub enum TokenProviderError { UnscopedBuilder { /// The source of the error. #[from] - source: crate::token::unscoped::UnscopedPayloadBuilderError, + source: crate::token::types::unscoped::UnscopedPayloadBuilderError, }, #[error(transparent)] ProjectScopeBuilder { /// The source of the error. #[from] - source: crate::token::project_scoped::ProjectScopePayloadBuilderError, + source: crate::token::types::project_scoped::ProjectScopePayloadBuilderError, }, #[error(transparent)] DomainScopeBuilder { /// The source of the error. #[from] - source: crate::token::domain_scoped::DomainScopePayloadBuilderError, + source: crate::token::types::domain_scoped::DomainScopePayloadBuilderError, }, #[error(transparent)] FederationUnscopedBuilder { /// The source of the error. #[from] - source: crate::token::federation_unscoped::FederationUnscopedPayloadBuilderError, + source: crate::token::types::federation_unscoped::FederationUnscopedPayloadBuilderError, }, #[error(transparent)] FederationProjectScopeBuilder { /// The source of the error. #[from] - source: crate::token::federation_project_scoped::FederationProjectScopePayloadBuilderError, + source: crate::token::types::federation_project_scoped::FederationProjectScopePayloadBuilderError, }, #[error(transparent)] FederationDomainScopeBuilder { /// The source of the error. #[from] - source: crate::token::federation_domain_scoped::FederationDomainScopePayloadBuilderError, + source: crate::token::types::federation_domain_scoped::FederationDomainScopePayloadBuilderError, }, #[error(transparent)] RestrictedBuilder { /// The source of the error. #[from] - source: crate::token::restricted::RestrictedPayloadBuilderError, + source: crate::token::types::restricted::RestrictedPayloadBuilderError, }, #[error(transparent)] diff --git a/src/token/mock.rs b/src/token/mock.rs new file mode 100644 index 00000000..80229df3 --- /dev/null +++ b/src/token/mock.rs @@ -0,0 +1,112 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//! Internal mock structures for the [TokenProvider]. + +use async_trait::async_trait; +use mockall::mock; +use sea_orm::DatabaseConnection; + +use super::error::TokenProviderError; +use crate::auth::{AuthenticatedInfo, AuthzInfo}; +use crate::config::Config; +use crate::provider::Provider; + +use super::{ + Token, TokenApi, TokenRestriction, TokenRestrictionCreate, TokenRestrictionListParameters, + TokenRestrictionUpdate, +}; + +mock! { + pub TokenProvider { + pub fn new(cfg: &Config) -> Result; + } + + #[async_trait] + impl TokenApi for TokenProvider { + async fn authenticate_by_token<'a>( + &self, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result; + + async fn validate_token<'a>( + &self, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result; + + #[mockall::concretize] + fn issue_token( + &self, + authentication_info: AuthenticatedInfo, + authz_info: AuthzInfo, + token_restriction: Option<&TokenRestriction> + ) -> Result; + + fn encode_token(&self, token: &Token) -> Result; + + async fn populate_role_assignments( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; + + async fn expand_token_information( + &self, + token: &Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result; + + async fn get_token_restriction<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + expand_roles: bool, + ) -> Result, TokenProviderError>; + + async fn list_token_restrictions<'a>( + &self, + db: &DatabaseConnection, + params: &TokenRestrictionListParameters, + ) -> Result, TokenProviderError>; + + async fn create_token_restriction<'a>( + &self, + db: &DatabaseConnection, + restriction: TokenRestrictionCreate, + ) -> Result; + + async fn update_token_restriction<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + restriction: TokenRestrictionUpdate, + ) -> Result; + + async fn delete_token_restriction<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), TokenProviderError>; + } + + impl Clone for TokenProvider { + fn clone(&self) -> Self; + } + +} diff --git a/src/token/mod.rs b/src/token/mod.rs index 593bd382..5ab52f2d 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -11,28 +11,20 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 +//! Token provider. use async_trait::async_trait; use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use chrono::{Local, TimeDelta}; -#[cfg(test)] -use mockall::mock; use sea_orm::DatabaseConnection; use uuid::Uuid; -pub mod application_credential; -pub mod domain_scoped; +pub mod backend; pub mod error; -pub mod federation_domain_scoped; -pub mod federation_project_scoped; -pub mod federation_unscoped; -pub mod fernet; -pub mod fernet_utils; -pub mod project_scoped; -pub mod restricted; +#[cfg(test)] +mod mock; mod token_restriction; pub mod types; -pub mod unscoped; use crate::assignment::{ AssignmentApi, @@ -47,25 +39,12 @@ use crate::resource::{ ResourceApi, types::{Domain, Project}, }; +use backend::{TokenBackend, fernet::FernetTokenProvider}; pub use error::TokenProviderError; -use types::TokenBackend; -pub use application_credential::ApplicationCredentialPayload; -pub use domain_scoped::{DomainScopePayload, DomainScopePayloadBuilder}; -pub use federation_domain_scoped::{ - FederationDomainScopePayload, FederationDomainScopePayloadBuilder, -}; -pub use federation_project_scoped::{ - FederationProjectScopePayload, FederationProjectScopePayloadBuilder, -}; -pub use federation_unscoped::{FederationUnscopedPayload, FederationUnscopedPayloadBuilder}; -pub use project_scoped::{ProjectScopePayload, ProjectScopePayloadBuilder}; -pub use restricted::{RestrictedPayload, RestrictedPayloadBuilder}; -pub use types::{ - Token, TokenRestriction, TokenRestrictionCreate, TokenRestrictionListParameters, - TokenRestrictionUpdate, -}; -pub use unscoped::{UnscopedPayload, UnscopedPayloadBuilder}; +pub use crate::token::types::*; +#[cfg(test)] +pub use mock::MockTokenProvider; #[derive(Clone, Debug)] pub struct TokenProvider { @@ -76,7 +55,7 @@ pub struct TokenProvider { impl TokenProvider { pub fn new(config: &Config) -> Result { let backend_driver = match config.token.provider { - TokenProviderType::Fernet => fernet::FernetTokenProvider::new(config.clone()), + TokenProviderType::Fernet => FernetTokenProvider::new(config.clone()), }; Ok(Self { config: config.clone(), @@ -349,89 +328,6 @@ impl TokenProvider { } } -#[async_trait] -pub trait TokenApi: Send + Sync + Clone { - async fn authenticate_by_token<'a>( - &self, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - ) -> Result; - - /// Validate the token - async fn validate_token<'a>( - &self, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - ) -> Result; - - /// Issue a token for given parameters - fn issue_token( - &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restriction: Option<&TokenRestriction>, - ) -> Result; - - /// Encode the token into the X-SubjectToken String - fn encode_token(&self, token: &Token) -> Result; - - /// Populate role assignments in the token that support that information - async fn populate_role_assignments( - &self, - token: &mut Token, - db: &DatabaseConnection, - provider: &Provider, - ) -> Result<(), TokenProviderError>; - - /// Populate additional information (project, domain, roles, etc) in the token that support - /// that information - async fn expand_token_information( - &self, - token: &Token, - db: &DatabaseConnection, - provider: &Provider, - ) -> Result; - - /// Get the token restriction by the ID. - async fn get_token_restriction<'a>( - &self, - db: &DatabaseConnection, - id: &'a str, - expand_roles: bool, - ) -> Result, TokenProviderError>; - - /// Create new token restriction. - async fn create_token_restriction<'a>( - &self, - db: &DatabaseConnection, - restriction: TokenRestrictionCreate, - ) -> Result; - - /// List token restrictions. - async fn list_token_restrictions<'a>( - &self, - db: &DatabaseConnection, - params: &TokenRestrictionListParameters, - ) -> Result, TokenProviderError>; - - /// Update token restriction by the ID. - async fn update_token_restriction<'a>( - &self, - db: &DatabaseConnection, - id: &'a str, - restriction: TokenRestrictionUpdate, - ) -> Result; - - /// Delete token restriction by the ID. - async fn delete_token_restriction<'a>( - &self, - db: &DatabaseConnection, - id: &'a str, - ) -> Result<(), TokenProviderError>; -} - #[async_trait] impl TokenApi for TokenProvider { /// Authenticate by token @@ -828,91 +724,6 @@ impl TokenApi for TokenProvider { } } -#[cfg(test)] -mock! { - pub TokenProvider { - pub fn new(cfg: &Config) -> Result; - } - - #[async_trait] - impl TokenApi for TokenProvider { - async fn authenticate_by_token<'a>( - &self, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - ) -> Result; - - async fn validate_token<'a>( - &self, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - ) -> Result; - - #[mockall::concretize] - fn issue_token( - &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restriction: Option<&TokenRestriction> - ) -> Result; - - fn encode_token(&self, token: &Token) -> Result; - - async fn populate_role_assignments( - &self, - token: &mut Token, - db: &DatabaseConnection, - provider: &Provider, - ) -> Result<(), TokenProviderError>; - - async fn expand_token_information( - &self, - token: &Token, - db: &DatabaseConnection, - provider: &Provider, - ) -> Result; - - async fn get_token_restriction<'a>( - &self, - db: &DatabaseConnection, - id: &'a str, - expand_roles: bool, - ) -> Result, TokenProviderError>; - - async fn list_token_restrictions<'a>( - &self, - db: &DatabaseConnection, - params: &TokenRestrictionListParameters, - ) -> Result, TokenProviderError>; - - async fn create_token_restriction<'a>( - &self, - db: &DatabaseConnection, - restriction: TokenRestrictionCreate, - ) -> Result; - - async fn update_token_restriction<'a>( - &self, - db: &DatabaseConnection, - id: &'a str, - restriction: TokenRestrictionUpdate, - ) -> Result; - - async fn delete_token_restriction<'a>( - &self, - db: &DatabaseConnection, - id: &'a str, - ) -> Result<(), TokenProviderError>; - } - - impl Clone for TokenProvider { - fn clone(&self) -> Self; - } - -} - #[cfg(test)] mod tests { use sea_orm::DatabaseConnection; diff --git a/src/token/restricted.rs b/src/token/restricted.rs deleted file mode 100644 index 38d497bc..00000000 --- a/src/token/restricted.rs +++ /dev/null @@ -1,180 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -use chrono::{DateTime, Utc}; -use derive_builder::Builder; -use rmp::{decode::read_pfix, encode::write_pfix}; -use serde::Serialize; -use std::io::Write; - -use crate::assignment::types::Role; -use crate::identity::types::UserResponse; -use crate::resource::types::Project; -use crate::token::{ - error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, - types::Token, -}; - -/// Restricted token payload -#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] -#[builder(setter(into))] -pub struct RestrictedPayload { - /// User ID. - pub user_id: String, - /// Authentication methods used to obtain the token. - #[builder(default, setter(name = _methods))] - pub methods: Vec, - /// Token audit IDs. - #[builder(default, setter(name = _audit_ids))] - pub audit_ids: Vec, - /// Token expiration datetime in UTC. - pub expires_at: DateTime, - /// ID of the token restrictions. - pub token_restriction_id: String, - /// Project ID scope for the token. - pub project_id: String, - /// Whether the token can be renewed. - pub allow_renew: bool, - /// Whether the token can be rescoped. - pub allow_rescope: bool, - - #[builder(default)] - pub issued_at: DateTime, - #[builder(default)] - pub user: Option, - #[builder(default)] - pub roles: Option>, - #[builder(default)] - pub project: Option, -} - -impl RestrictedPayloadBuilder { - pub fn methods(&mut self, iter: I) -> &mut Self - where - I: Iterator, - V: Into, - { - self.methods - .get_or_insert_with(Vec::new) - .extend(iter.map(Into::into)); - self - } - - pub fn audit_ids(&mut self, iter: I) -> &mut Self - where - I: Iterator, - V: Into, - { - self.audit_ids - .get_or_insert_with(Vec::new) - .extend(iter.map(Into::into)); - self - } -} - -impl From for Token { - fn from(value: RestrictedPayload) -> Self { - Self::Restricted(value) - } -} - -impl MsgPackToken for RestrictedPayload { - type Token = Self; - - fn assemble( - &self, - wd: &mut W, - fernet_provider: &FernetTokenProvider, - ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; - write_pfix( - wd, - fernet_provider.encode_auth_methods(self.methods.clone())?, - ) - .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_uuid(wd, &self.token_restriction_id)?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_uuid(wd, &self.project_id)?; - fernet_utils::write_bool(wd, self.allow_renew)?; - fernet_utils::write_bool(wd, self.allow_rescope)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; - - Ok(()) - } - - fn disassemble( - rd: &mut &[u8], - fernet_provider: &FernetTokenProvider, - ) -> Result { - // Order of reading is important - let user_id = fernet_utils::read_uuid(rd)?; - let methods: Vec = fernet_provider - .decode_auth_methods(read_pfix(rd)?)? - .into_iter() - .collect(); - let token_restriction_id = fernet_utils::read_uuid(rd)?; - let expires_at = fernet_utils::read_time(rd)?; - let project_id = fernet_utils::read_uuid(rd)?; - let allow_renew = fernet_utils::read_bool(rd)?; - let allow_rescope = fernet_utils::read_bool(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); - Ok(Self { - user_id, - methods, - expires_at, - audit_ids, - token_restriction_id, - project_id, - allow_renew, - allow_rescope, - - ..Default::default() - }) - } -} - -#[cfg(test)] -mod tests { - use chrono::{Local, SubsecRound}; - use uuid::Uuid; - - use super::super::tests::setup_config; - use super::*; - - #[test] - fn test_roundtrip() { - let token = RestrictedPayload { - user_id: Uuid::new_v4().simple().to_string(), - methods: vec!["openid".into()], - audit_ids: vec!["Zm9vCg".into()], - expires_at: Local::now().trunc_subsecs(0).into(), - token_restriction_id: "trid".into(), - project_id: "pid".into(), - allow_renew: true, - allow_rescope: true, - ..Default::default() - }; - - let provider = FernetTokenProvider::new(setup_config()); - - let mut buf = vec![]; - token.assemble(&mut buf, &provider).unwrap(); - let encoded_buf = buf.clone(); - let decoded = - RestrictedPayload::disassemble(&mut encoded_buf.as_slice(), &provider).unwrap(); - assert_eq!(token, decoded); - } -} diff --git a/src/token/types.rs b/src/token/types.rs index 1668df57..115e15b3 100644 --- a/src/token/types.rs +++ b/src/token/types.rs @@ -11,36 +11,58 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 +//! Token provider types. use chrono::{DateTime, Utc}; -use derive_builder::Builder; -use dyn_clone::DynClone; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use crate::assignment::types::Role; -use crate::config::Config; use crate::identity::types::UserResponse; use crate::resource::types::{Domain, Project}; -use crate::token::TokenProviderError; -use crate::token::application_credential::ApplicationCredentialPayload; -use crate::token::domain_scoped::DomainScopePayload; -use crate::token::federation_domain_scoped::FederationDomainScopePayload; -use crate::token::federation_project_scoped::FederationProjectScopePayload; -use crate::token::federation_unscoped::FederationUnscopedPayload; -use crate::token::project_scoped::ProjectScopePayload; -use crate::token::restricted::RestrictedPayload; -use crate::token::unscoped::UnscopedPayload; +pub mod application_credential; +pub mod domain_scoped; +pub mod federation_domain_scoped; +pub mod federation_project_scoped; +pub mod federation_unscoped; +pub mod project_scoped; +pub mod provider_api; +pub mod restricted; +pub mod unscoped; + +pub use application_credential::ApplicationCredentialPayload; +pub use domain_scoped::{DomainScopePayload, DomainScopePayloadBuilder}; +pub use federation_domain_scoped::{ + FederationDomainScopePayload, FederationDomainScopePayloadBuilder, +}; +pub use federation_project_scoped::{ + FederationProjectScopePayload, FederationProjectScopePayloadBuilder, +}; +pub use federation_unscoped::{FederationUnscopedPayload, FederationUnscopedPayloadBuilder}; +pub use project_scoped::{ProjectScopePayload, ProjectScopePayloadBuilder}; +pub use provider_api::TokenApi; +pub use restricted::*; +pub use unscoped::*; + +/// Fernet Token. #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(untagged)] pub enum Token { + /// Unscoped. Unscoped(UnscopedPayload), + /// Domain scoped. DomainScope(DomainScopePayload), + /// Project scoped. ProjectScope(ProjectScopePayload), + /// Federated unscoped. FederationUnscoped(FederationUnscopedPayload), + /// Federated project scoped. FederationProjectScope(FederationProjectScopePayload), + /// Federated domain scoped. FederationDomainScope(FederationDomainScopePayload), + /// Application credential. ApplicationCredential(ApplicationCredentialPayload), + /// Restricted. Restricted(RestrictedPayload), } @@ -174,82 +196,3 @@ impl Token { } } } - -/// Token restriction information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct TokenRestriction { - /// Whether the restriction allows to rescope the token. - pub allow_rescope: bool, - /// Whether it is allowed to renew the token with this restriction. - pub allow_renew: bool, - /// Id. - pub id: String, - /// Domain Id the token restriction belongs to. - pub domain_id: String, - /// Optional project ID to be used with this restriction. - pub project_id: Option, - /// Roles bound to the restriction. - pub role_ids: Vec, - /// Optional list of full Role information. - pub roles: Option>, - /// User id - pub user_id: Option, -} - -/// New token restriction information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct TokenRestrictionCreate { - /// Whether the restriction allows to rescope the token. - pub allow_rescope: bool, - /// Whether it is allowed to renew the token with this restriction. - pub allow_renew: bool, - /// Id. - pub id: String, - /// Domain Id the token restriction belongs to. - pub domain_id: String, - /// Optional project ID to be used with this restriction. - pub project_id: Option, - /// Roles bound to the restriction. - pub role_ids: Vec, - /// User id - pub user_id: Option, -} - -/// Token restriction update information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct TokenRestrictionUpdate { - /// Whether the restriction allows to rescope the token. - pub allow_rescope: Option, - /// Whether it is allowed to renew the token with this restriction. - pub allow_renew: Option, - /// Optional project ID to be used with this restriction. - pub project_id: Option>, - /// Roles bound to the restriction. - pub role_ids: Option>, - /// User id. - pub user_id: Option>, -} - -/// Token restriction list filters. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct TokenRestrictionListParameters { - /// Domain id. - pub domain_id: Option, - /// User id. - pub user_id: Option, - /// Project id. - pub project_id: Option, -} - -pub trait TokenBackend: DynClone + Send + Sync + std::fmt::Debug { - /// Set config - fn set_config(&mut self, g: Config); - - /// Extract the token from string - fn decode(&self, credential: &str) -> Result; - - /// Extract the token from string - fn encode(&self, token: &Token) -> Result; -} - -dyn_clone::clone_trait_object!(TokenBackend); diff --git a/src/token/types/application_credential.rs b/src/token/types/application_credential.rs new file mode 100644 index 00000000..9c6ea338 --- /dev/null +++ b/src/token/types/application_credential.rs @@ -0,0 +1,74 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use serde::Serialize; + +use crate::assignment::types::Role; +use crate::identity::types::UserResponse; +use crate::resource::types::Project; +use crate::token::types::Token; + +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] +#[builder(setter(into))] +pub struct ApplicationCredentialPayload { + pub user_id: String, + #[builder(default, setter(name = _methods))] + pub methods: Vec, + #[builder(default, setter(name = _audit_ids))] + pub audit_ids: Vec, + pub expires_at: DateTime, + pub project_id: String, + pub application_credential_id: String, + + #[builder(default)] + pub issued_at: DateTime, + #[builder(default)] + pub user: Option, + #[builder(default)] + pub roles: Vec, + #[builder(default)] + pub project: Option, +} + +impl ApplicationCredentialPayloadBuilder { + pub fn methods(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.methods + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } + + pub fn audit_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.audit_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } +} + +impl From for Token { + fn from(value: ApplicationCredentialPayload) -> Self { + Self::ApplicationCredential(value) + } +} diff --git a/src/token/domain_scoped.rs b/src/token/types/domain_scoped.rs similarity index 87% rename from src/token/domain_scoped.rs rename to src/token/types/domain_scoped.rs index 7a9a1883..143265db 100644 --- a/src/token/domain_scoped.rs +++ b/src/token/types/domain_scoped.rs @@ -22,9 +22,8 @@ use crate::assignment::types::Role; use crate::identity::types::UserResponse; use crate::resource::types::Domain; use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, types::Token, }; @@ -87,15 +86,15 @@ impl MsgPackToken for DomainScopePayload { wd: &mut W, fernet_provider: &FernetTokenProvider, ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; + utils::write_uuid(wd, &self.user_id)?; write_pfix( wd, fernet_provider.encode_auth_methods(self.methods.clone())?, ) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_uuid(wd, &self.domain_id)?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_uuid(wd, &self.domain_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; Ok(()) } @@ -105,14 +104,14 @@ impl MsgPackToken for DomainScopePayload { fernet_provider: &FernetTokenProvider, ) -> Result { // Order of reading is important - let user_id = fernet_utils::read_uuid(rd)?; + let user_id = utils::read_uuid(rd)?; let methods: Vec = fernet_provider .decode_auth_methods(read_pfix(rd)?)? .into_iter() .collect(); - let domain_id = fernet_utils::read_uuid(rd)?; - let expires_at = fernet_utils::read_time(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); + let domain_id = utils::read_uuid(rd)?; + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); Ok(Self { user_id, methods, @@ -129,8 +128,8 @@ mod tests { use chrono::{Local, SubsecRound}; use uuid::Uuid; - use super::super::tests::setup_config; use super::*; + use crate::token::tests::setup_config; #[test] fn test_roundtrip() { diff --git a/src/token/federation_domain_scoped.rs b/src/token/types/federation_domain_scoped.rs similarity index 82% rename from src/token/federation_domain_scoped.rs rename to src/token/types/federation_domain_scoped.rs index 2f07cc06..a3c74e57 100644 --- a/src/token/federation_domain_scoped.rs +++ b/src/token/types/federation_domain_scoped.rs @@ -22,9 +22,8 @@ use crate::assignment::types::Role; use crate::identity::types::UserResponse; use crate::resource::types::Domain; use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, types::Token, }; @@ -91,18 +90,18 @@ impl MsgPackToken for FederationDomainScopePayload { wd: &mut W, fernet_provider: &FernetTokenProvider, ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; + utils::write_uuid(wd, &self.user_id)?; write_pfix( wd, fernet_provider.encode_auth_methods(self.methods.clone())?, ) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_uuid(wd, &self.domain_id)?; - fernet_utils::write_list_of_uuids(wd, self.group_ids.iter())?; - fernet_utils::write_uuid(wd, &self.idp_id)?; - fernet_utils::write_str(wd, &self.protocol_id)?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_uuid(wd, &self.domain_id)?; + utils::write_list_of_uuids(wd, self.group_ids.iter())?; + utils::write_uuid(wd, &self.idp_id)?; + utils::write_str(wd, &self.protocol_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; Ok(()) } @@ -112,17 +111,17 @@ impl MsgPackToken for FederationDomainScopePayload { fernet_provider: &FernetTokenProvider, ) -> Result { // Order of reading is important - let user_id = fernet_utils::read_uuid(rd)?; + let user_id = utils::read_uuid(rd)?; let methods: Vec = fernet_provider .decode_auth_methods(read_pfix(rd)?)? .into_iter() .collect(); - let domain_id = fernet_utils::read_uuid(rd)?; - let group_ids = fernet_utils::read_list_of_uuids(rd)?; - let idp_id = fernet_utils::read_uuid(rd)?; - let protocol_id = fernet_utils::read_str(rd)?; - let expires_at = fernet_utils::read_time(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); + let domain_id = utils::read_uuid(rd)?; + let group_ids = utils::read_list_of_uuids(rd)?; + let idp_id = utils::read_uuid(rd)?; + let protocol_id = utils::read_str(rd)?; + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); Ok(Self { user_id, methods, @@ -142,8 +141,8 @@ mod tests { use chrono::{Local, SubsecRound}; use uuid::Uuid; - use super::super::tests::setup_config; use super::*; + use crate::token::tests::setup_config; #[test] fn test_roundtrip() { diff --git a/src/token/federation_project_scoped.rs b/src/token/types/federation_project_scoped.rs similarity index 82% rename from src/token/federation_project_scoped.rs rename to src/token/types/federation_project_scoped.rs index 464b86ba..392ec36b 100644 --- a/src/token/federation_project_scoped.rs +++ b/src/token/types/federation_project_scoped.rs @@ -22,9 +22,8 @@ use crate::assignment::types::Role; use crate::identity::types::UserResponse; use crate::resource::types::Project; use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, types::Token, }; @@ -91,18 +90,18 @@ impl MsgPackToken for FederationProjectScopePayload { wd: &mut W, fernet_provider: &FernetTokenProvider, ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; + utils::write_uuid(wd, &self.user_id)?; write_pfix( wd, fernet_provider.encode_auth_methods(self.methods.clone())?, ) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_uuid(wd, &self.project_id)?; - fernet_utils::write_list_of_uuids(wd, self.group_ids.iter())?; - fernet_utils::write_uuid(wd, &self.idp_id)?; - fernet_utils::write_str(wd, &self.protocol_id)?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_uuid(wd, &self.project_id)?; + utils::write_list_of_uuids(wd, self.group_ids.iter())?; + utils::write_uuid(wd, &self.idp_id)?; + utils::write_str(wd, &self.protocol_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; Ok(()) } @@ -112,17 +111,17 @@ impl MsgPackToken for FederationProjectScopePayload { fernet_provider: &FernetTokenProvider, ) -> Result { // Order of reading is important - let user_id = fernet_utils::read_uuid(rd)?; + let user_id = utils::read_uuid(rd)?; let methods: Vec = fernet_provider .decode_auth_methods(read_pfix(rd)?)? .into_iter() .collect(); - let project_id = fernet_utils::read_uuid(rd)?; - let group_ids = fernet_utils::read_list_of_uuids(rd)?; - let idp_id = fernet_utils::read_uuid(rd)?; - let protocol_id = fernet_utils::read_str(rd)?; - let expires_at = fernet_utils::read_time(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); + let project_id = utils::read_uuid(rd)?; + let group_ids = utils::read_list_of_uuids(rd)?; + let idp_id = utils::read_uuid(rd)?; + let protocol_id = utils::read_str(rd)?; + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); Ok(Self { user_id, methods, @@ -142,8 +141,8 @@ mod tests { use chrono::{Local, SubsecRound}; use uuid::Uuid; - use super::super::tests::setup_config; use super::*; + use crate::token::tests::setup_config; #[test] fn test_roundtrip() { diff --git a/src/token/federation_unscoped.rs b/src/token/types/federation_unscoped.rs similarity index 83% rename from src/token/federation_unscoped.rs rename to src/token/types/federation_unscoped.rs index 714c57b6..4ab62739 100644 --- a/src/token/federation_unscoped.rs +++ b/src/token/types/federation_unscoped.rs @@ -20,9 +20,8 @@ use std::io::Write; use crate::identity::types::UserResponse; use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, types::Token, }; @@ -84,17 +83,17 @@ impl MsgPackToken for FederationUnscopedPayload { wd: &mut W, fernet_provider: &FernetTokenProvider, ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; + utils::write_uuid(wd, &self.user_id)?; write_pfix( wd, fernet_provider.encode_auth_methods(self.methods.clone())?, ) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_list_of_uuids(wd, self.group_ids.iter())?; - fernet_utils::write_uuid(wd, &self.idp_id)?; - fernet_utils::write_str(wd, &self.protocol_id)?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_list_of_uuids(wd, self.group_ids.iter())?; + utils::write_uuid(wd, &self.idp_id)?; + utils::write_str(wd, &self.protocol_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; Ok(()) } @@ -104,16 +103,16 @@ impl MsgPackToken for FederationUnscopedPayload { fernet_provider: &FernetTokenProvider, ) -> Result { // Order of reading is important - let user_id = fernet_utils::read_uuid(rd)?; + let user_id = utils::read_uuid(rd)?; let methods: Vec = fernet_provider .decode_auth_methods(read_pfix(rd)?)? .into_iter() .collect(); - let group_ids = fernet_utils::read_list_of_uuids(rd)?; - let idp_id = fernet_utils::read_uuid(rd)?; - let protocol_id = fernet_utils::read_str(rd)?; - let expires_at = fernet_utils::read_time(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); + let group_ids = utils::read_list_of_uuids(rd)?; + let idp_id = utils::read_uuid(rd)?; + let protocol_id = utils::read_str(rd)?; + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); Ok(Self { user_id, methods, @@ -132,8 +131,8 @@ mod tests { use chrono::{Local, SubsecRound}; use uuid::Uuid; - use super::super::tests::setup_config; use super::*; + use crate::token::tests::setup_config; #[test] fn test_roundtrip() { diff --git a/src/token/project_scoped.rs b/src/token/types/project_scoped.rs similarity index 87% rename from src/token/project_scoped.rs rename to src/token/types/project_scoped.rs index af685c67..c27a6e74 100644 --- a/src/token/project_scoped.rs +++ b/src/token/types/project_scoped.rs @@ -22,9 +22,8 @@ use crate::assignment::types::Role; use crate::identity::types::UserResponse; use crate::resource::types::Project; use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, types::Token, }; @@ -87,15 +86,15 @@ impl MsgPackToken for ProjectScopePayload { wd: &mut W, fernet_provider: &FernetTokenProvider, ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; + utils::write_uuid(wd, &self.user_id)?; write_pfix( wd, fernet_provider.encode_auth_methods(self.methods.clone())?, ) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_uuid(wd, &self.project_id)?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_uuid(wd, &self.project_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; Ok(()) } @@ -105,14 +104,14 @@ impl MsgPackToken for ProjectScopePayload { fernet_provider: &FernetTokenProvider, ) -> Result { // Order of reading is important - let user_id = fernet_utils::read_uuid(rd)?; + let user_id = utils::read_uuid(rd)?; let methods: Vec = fernet_provider .decode_auth_methods(read_pfix(rd)?)? .into_iter() .collect(); - let project_id = fernet_utils::read_uuid(rd)?; - let expires_at = fernet_utils::read_time(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); + let project_id = utils::read_uuid(rd)?; + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); Ok(Self { user_id, methods, @@ -129,8 +128,8 @@ mod tests { use chrono::{Local, SubsecRound}; use uuid::Uuid; - use super::super::tests::setup_config; use super::*; + use crate::token::tests::setup_config; #[test] fn test_roundtrip() { diff --git a/src/token/types/provider_api.rs b/src/token/types/provider_api.rs new file mode 100644 index 00000000..ea68ff61 --- /dev/null +++ b/src/token/types/provider_api.rs @@ -0,0 +1,107 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//! Token provider types. + +use async_trait::async_trait; +use sea_orm::DatabaseConnection; + +use crate::auth::{AuthenticatedInfo, AuthzInfo}; +use crate::provider::Provider; +use crate::token::TokenProviderError; + +use super::*; + +/// Token Provider interface. +#[async_trait] +pub trait TokenApi: Send + Sync + Clone { + async fn authenticate_by_token<'a>( + &self, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result; + + /// Validate the token + async fn validate_token<'a>( + &self, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result; + + /// Issue a token for given parameters + fn issue_token( + &self, + authentication_info: AuthenticatedInfo, + authz_info: AuthzInfo, + token_restriction: Option<&TokenRestriction>, + ) -> Result; + + /// Encode the token into the X-SubjectToken String + fn encode_token(&self, token: &Token) -> Result; + + /// Populate role assignments in the token that support that information + async fn populate_role_assignments( + &self, + token: &mut Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result<(), TokenProviderError>; + + /// Populate additional information (project, domain, roles, etc) in the token that support + /// that information + async fn expand_token_information( + &self, + token: &Token, + db: &DatabaseConnection, + provider: &Provider, + ) -> Result; + + /// Get the token restriction by the ID. + async fn get_token_restriction<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + expand_roles: bool, + ) -> Result, TokenProviderError>; + + /// Create new token restriction. + async fn create_token_restriction<'a>( + &self, + db: &DatabaseConnection, + restriction: TokenRestrictionCreate, + ) -> Result; + + /// List token restrictions. + async fn list_token_restrictions<'a>( + &self, + db: &DatabaseConnection, + params: &TokenRestrictionListParameters, + ) -> Result, TokenProviderError>; + + /// Update token restriction by the ID. + async fn update_token_restriction<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + restriction: TokenRestrictionUpdate, + ) -> Result; + + /// Delete token restriction by the ID. + async fn delete_token_restriction<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), TokenProviderError>; +} diff --git a/src/token/types/restricted.rs b/src/token/types/restricted.rs new file mode 100644 index 00000000..133aef37 --- /dev/null +++ b/src/token/types/restricted.rs @@ -0,0 +1,152 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//! Restricted token types. + +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::assignment::types::Role; +use crate::identity::types::UserResponse; +use crate::resource::types::Project; +use crate::token::types::Token; + +/// Restricted token payload. +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize)] +#[builder(setter(into))] +pub struct RestrictedPayload { + /// User ID. + pub user_id: String, + /// Authentication methods used to obtain the token. + #[builder(default, setter(name = _methods))] + pub methods: Vec, + /// Token audit IDs. + #[builder(default, setter(name = _audit_ids))] + pub audit_ids: Vec, + /// Token expiration datetime in UTC. + pub expires_at: DateTime, + /// ID of the token restrictions. + pub token_restriction_id: String, + /// Project ID scope for the token. + pub project_id: String, + /// Whether the token can be renewed. + pub allow_renew: bool, + /// Whether the token can be rescoped. + pub allow_rescope: bool, + + #[builder(default)] + pub issued_at: DateTime, + #[builder(default)] + pub user: Option, + #[builder(default)] + pub roles: Option>, + #[builder(default)] + pub project: Option, +} + +impl RestrictedPayloadBuilder { + pub fn methods(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.methods + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } + + pub fn audit_ids(&mut self, iter: I) -> &mut Self + where + I: Iterator, + V: Into, + { + self.audit_ids + .get_or_insert_with(Vec::new) + .extend(iter.map(Into::into)); + self + } +} + +impl From for Token { + fn from(value: RestrictedPayload) -> Self { + Self::Restricted(value) + } +} + +/// Token restriction information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct TokenRestriction { + /// Whether the restriction allows to rescope the token. + pub allow_rescope: bool, + /// Whether it is allowed to renew the token with this restriction. + pub allow_renew: bool, + /// Id. + pub id: String, + /// Domain Id the token restriction belongs to. + pub domain_id: String, + /// Optional project ID to be used with this restriction. + pub project_id: Option, + /// Roles bound to the restriction. + pub role_ids: Vec, + /// Optional list of full Role information. + pub roles: Option>, + /// User id + pub user_id: Option, +} + +/// New token restriction information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct TokenRestrictionCreate { + /// Whether the restriction allows to rescope the token. + pub allow_rescope: bool, + /// Whether it is allowed to renew the token with this restriction. + pub allow_renew: bool, + /// Id. + pub id: String, + /// Domain Id the token restriction belongs to. + pub domain_id: String, + /// Optional project ID to be used with this restriction. + pub project_id: Option, + /// Roles bound to the restriction. + pub role_ids: Vec, + /// User id + pub user_id: Option, +} + +/// Token restriction update information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct TokenRestrictionUpdate { + /// Whether the restriction allows to rescope the token. + pub allow_rescope: Option, + /// Whether it is allowed to renew the token with this restriction. + pub allow_renew: Option, + /// Optional project ID to be used with this restriction. + pub project_id: Option>, + /// Roles bound to the restriction. + pub role_ids: Option>, + /// User id. + pub user_id: Option>, +} + +/// Token restriction list filters. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct TokenRestrictionListParameters { + /// Domain id. + pub domain_id: Option, + /// User id. + pub user_id: Option, + /// Project id. + pub project_id: Option, +} diff --git a/src/token/unscoped.rs b/src/token/types/unscoped.rs similarity index 88% rename from src/token/unscoped.rs rename to src/token/types/unscoped.rs index 560f9ff7..8ded3ff7 100644 --- a/src/token/unscoped.rs +++ b/src/token/types/unscoped.rs @@ -20,9 +20,8 @@ use std::io::Write; use crate::identity::types::UserResponse; use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, error::TokenProviderError, - fernet::{FernetTokenProvider, MsgPackToken}, - fernet_utils, types::Token, }; @@ -80,14 +79,14 @@ impl MsgPackToken for UnscopedPayload { wd: &mut W, fernet_provider: &FernetTokenProvider, ) -> Result<(), TokenProviderError> { - fernet_utils::write_uuid(wd, &self.user_id)?; + utils::write_uuid(wd, &self.user_id)?; write_pfix( wd, fernet_provider.encode_auth_methods(self.methods.clone())?, ) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; - fernet_utils::write_time(wd, self.expires_at)?; - fernet_utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; Ok(()) } @@ -97,13 +96,13 @@ impl MsgPackToken for UnscopedPayload { fernet_provider: &FernetTokenProvider, ) -> Result { // Order of writing is important - let user_id = fernet_utils::read_uuid(rd)?; + let user_id = utils::read_uuid(rd)?; let methods: Vec = fernet_provider .decode_auth_methods(read_pfix(rd)?)? .into_iter() .collect(); - let expires_at = fernet_utils::read_time(rd)?; - let audit_ids: Vec = fernet_utils::read_audit_ids(rd)?.into_iter().collect(); + let expires_at = utils::read_time(rd)?; + let audit_ids: Vec = utils::read_audit_ids(rd)?.into_iter().collect(); Ok(Self::Token { user_id, methods, @@ -119,8 +118,8 @@ mod tests { use chrono::{Local, SubsecRound}; use uuid::Uuid; - use super::super::tests::setup_config; use super::*; + use crate::token::tests::setup_config; #[test] fn test_roundtrip() {