diff --git a/src/api/common.rs b/src/api/common.rs index 4496654..ff04106 100644 --- a/src/api/common.rs +++ b/src/api/common.rs @@ -175,8 +175,8 @@ pub trait QueryParameterPagination { fn set_marker(&mut self, marker: String) -> &mut Self; } -/// Trait for the resource to expose the unique identifier that can be used for building the -/// marker pagination. +/// Trait for the resource to expose the unique identifier that can be used for +/// building the marker pagination. pub trait ResourceIdentifier { /// Get the unique resource identifier. fn get_id(&self) -> String; diff --git a/src/api/v3/auth/token/token_impl.rs b/src/api/v3/auth/token/token_impl.rs index 319227d..e50d0c7 100644 --- a/src/api/v3/auth/token/token_impl.rs +++ b/src/api/v3/auth/token/token_impl.rs @@ -150,6 +150,21 @@ impl Token { ); } } + ProviderToken::Trust(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); + } + } } if let Some(domain) = domain { diff --git a/src/api/v4/auth/token/token_impl.rs b/src/api/v4/auth/token/token_impl.rs index 205eb05..0a3dcc5 100644 --- a/src/api/v4/auth/token/token_impl.rs +++ b/src/api/v4/auth/token/token_impl.rs @@ -150,6 +150,21 @@ impl Token { ); } } + ProviderToken::Trust(token) => { + if project.is_none() { + project = Some( + state + .provider + .get_resource_provider() + .get_project(state, &token.project_id) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: token.project_id.clone(), + })?, + ); + } + } } if let Some(domain) = domain { diff --git a/src/application_credential/backend/sql/application_credential.rs b/src/application_credential/backend/sql/application_credential.rs index b2a4551..4cf758f 100644 --- a/src/application_credential/backend/sql/application_credential.rs +++ b/src/application_credential/backend/sql/application_credential.rs @@ -137,6 +137,7 @@ pub(crate) mod tests { unrestricted: Some(true), } } + pub fn get_application_credential_mock_from_active( active: application_credential::ActiveModel, internal_id: i32, diff --git a/src/application_credential/backend/sql/application_credential/get.rs b/src/application_credential/backend/sql/application_credential/get.rs index 3c77579..aec3adb 100644 --- a/src/application_credential/backend/sql/application_credential/get.rs +++ b/src/application_credential/backend/sql/application_credential/get.rs @@ -81,7 +81,7 @@ mod tests { "app_cred_id", Some(12345), )]]) - .append_query_results([vec![get_role_mock("role_id".into())]]) + .append_query_results([vec![get_role_mock("role_id")]]) .append_query_results([vec![get_access_rule_mock("app_cred_rule_id", None)]]) .into_connection(); diff --git a/src/application_credential/backend/sql/application_credential/list.rs b/src/application_credential/backend/sql/application_credential/list.rs index 1fc38ac..375e1b9 100644 --- a/src/application_credential/backend/sql/application_credential/list.rs +++ b/src/application_credential/backend/sql/application_credential/list.rs @@ -69,8 +69,8 @@ pub async fn list( let roles = roles_handle .context("fetching roles for application credential list")? .into_iter() - .map(|apc| { - apc.into_iter() + .map(|apr| { + apr.into_iter() .map(TryInto::::try_into) .collect::, _>>() }) @@ -133,10 +133,7 @@ mod tests { role_id: "role_id2".into(), }, ]]) - .append_query_results([vec![ - get_role_mock("role_id1".into()), - get_role_mock("role_id2".into()), - ]]) + .append_query_results([vec![get_role_mock("role_id1"), get_role_mock("role_id2")]]) .append_query_results([vec![ db_application_credential_access_rule::Model { application_credential_id: 1, diff --git a/src/assignment/backend/sql/role.rs b/src/assignment/backend/sql/role.rs index 941b79b..e8ac729 100644 --- a/src/assignment/backend/sql/role.rs +++ b/src/assignment/backend/sql/role.rs @@ -62,9 +62,9 @@ pub(crate) mod tests { use super::*; - pub(crate) fn get_role_mock(id: String) -> role::Model { + pub fn get_role_mock>(id: S) -> role::Model { role::Model { - id: id.clone(), + id: id.as_ref().into(), domain_id: "foo_domain".into(), name: "foo".into(), ..Default::default() @@ -77,7 +77,7 @@ pub(crate) mod tests { let db = MockDatabase::new(DatabaseBackend::Postgres) .append_query_results([ // First query result - select user itself - vec![get_role_mock("1".into())], + vec![get_role_mock("1")], ]) .into_connection(); let config = Config::default(); @@ -108,15 +108,15 @@ pub(crate) mod tests { let db = MockDatabase::new(DatabaseBackend::Postgres) .append_query_results([ // First query result - select user itself - vec![get_role_mock("1".into())], + vec![get_role_mock("1")], ]) .append_query_results([ // First query result - select user itself - vec![get_role_mock("1".into())], + vec![get_role_mock("1")], ]) .append_query_results([ // First query result - select user itself - vec![get_role_mock("1".into())], + vec![get_role_mock("1")], ]) .into_connection(); let config = Config::default(); diff --git a/src/assignment/backend/sql/role/create.rs b/src/assignment/backend/sql/role/create.rs index f7c58b0..f69a836 100644 --- a/src/assignment/backend/sql/role/create.rs +++ b/src/assignment/backend/sql/role/create.rs @@ -109,7 +109,8 @@ mod tests { let created = create(&db, role_create).await.unwrap(); assert_eq!(created.name, "Global Role"); - // domain_id should be None in the returned Role (because TryFrom filters NULL_DOMAIN_ID) + // domain_id should be None in the returned Role (because TryFrom filters + // NULL_DOMAIN_ID) assert_eq!(created.domain_id, None); } diff --git a/src/config.rs b/src/config.rs index 264d138..02f707e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -81,6 +81,10 @@ pub struct Config { /// Token provider configuration. #[serde(default)] pub token: TokenProvider, + + /// Trust provider configuration. + #[serde(default)] + pub trust: TrustProvider, } /// Default configuration section. @@ -588,6 +592,22 @@ pub enum TokenProviderDriver { Fernet, } +/// Trust provider. +#[derive(Debug, Deserialize, Clone)] +pub struct TrustProvider { + /// Trust provider driver. + #[serde(default = "default_sql_driver")] + pub driver: String, +} + +impl Default for TrustProvider { + fn default() -> Self { + Self { + driver: default_sql_driver(), + } + } +} + impl Config { pub fn new(path: PathBuf) -> Result { let mut builder = config::Config::builder(); diff --git a/src/db/entity/trust.rs b/src/db/entity/trust.rs index ead0b2f..2c48a72 100644 --- a/src/db/entity/trust.rs +++ b/src/db/entity/trust.rs @@ -27,15 +27,34 @@ pub struct Model { pub impersonation: bool, pub deleted_at: Option, pub expires_at: Option, - pub remaining_uses: Option, + pub remaining_uses: Option, #[sea_orm(column_type = "Text", nullable)] pub extra: Option, pub expires_at_int: Option, pub redelegated_trust_id: Option, - pub redelegation_count: Option, + pub redelegation_count: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::trust_role::Entity")] + TrustRole, +} impl ActiveModelBehavior for ActiveModel {} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::TrustRole.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::trust_role::Relation::Role.def() + } + + fn via() -> Option { + Some(super::trust_role::Relation::Trust.def().rev()) + } +} diff --git a/src/db/entity/trust_role.rs b/src/db/entity/trust_role.rs index 375898a..96afd97 100644 --- a/src/db/entity/trust_role.rs +++ b/src/db/entity/trust_role.rs @@ -26,6 +26,35 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::trust::Entity", + from = "Column::TrustId", + to = "super::trust::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Trust, + #[sea_orm( + belongs_to = "super::role::Entity", + from = "Column::RoleId", + to = "super::role::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Role, +} impl ActiveModelBehavior for ActiveModel {} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Trust.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Role.def() + } +} diff --git a/src/lib.rs b/src/lib.rs index 1ed4f10..e8ecf55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,6 +91,7 @@ pub mod provider; pub mod resource; pub mod revoke; pub mod token; +pub mod trust; pub mod webauthn; #[cfg(test)] diff --git a/src/plugin_manager.rs b/src/plugin_manager.rs index c604532..aba7fe4 100644 --- a/src/plugin_manager.rs +++ b/src/plugin_manager.rs @@ -29,6 +29,7 @@ use crate::federation::backend::FederationBackend; use crate::identity::backends::IdentityBackend; use crate::resource::types::ResourceBackend; use crate::revoke::backend::RevokeBackend; +use crate::trust::backend::TrustBackend; /// Plugin manager allowing to pass custom backend plugins implementing required /// trait during the service start. @@ -48,6 +49,8 @@ pub struct PluginManager { resource_backends: HashMap>, /// Revoke backend plugins. revoke_backends: HashMap>, + /// Trust backend plugins. + trust_backends: HashMap>, } impl PluginManager { @@ -117,4 +120,10 @@ impl PluginManager { pub fn get_revoke_backend>(&self, name: S) -> Option<&Box> { self.revoke_backends.get(name.as_ref()) } + + /// Get registered trust backend. + #[allow(clippy::borrowed_box)] + pub fn get_trust_backend>(&self, name: S) -> Option<&Box> { + self.trust_backends.get(name.as_ref()) + } } diff --git a/src/token/backend/fernet.rs b/src/token/backend/fernet.rs index f6e3989..58c02c2 100644 --- a/src/token/backend/fernet.rs +++ b/src/token/backend/fernet.rs @@ -39,13 +39,14 @@ use crate::token::{ federation_domain_scoped::FederationDomainScopePayload, federation_project_scoped::FederationProjectScopePayload, federation_unscoped::FederationUnscopedPayload, project_scoped::ProjectScopePayload, - restricted::RestrictedPayload, unscoped::UnscopedPayload, *, + restricted::RestrictedPayload, trust::TrustPayload, unscoped::UnscopedPayload, *, }, }; use utils::FernetUtils; mod application_credential; mod restricted; +mod trust; pub mod utils; #[derive(Clone)] @@ -216,6 +217,7 @@ impl FernetTokenProvider { 0 => Ok(UnscopedPayload::disassemble(rd, self)?.into()), 1 => Ok(DomainScopePayload::disassemble(rd, self)?.into()), 2 => Ok(ProjectScopePayload::disassemble(rd, self)?.into()), + 3 => Ok(TrustPayload::disassemble(rd, self)?.into()), 4 => Ok(FederationUnscopedPayload::disassemble(rd, self)?.into()), 5 => Ok(FederationProjectScopePayload::disassemble(rd, self)?.into()), 6 => Ok(FederationDomainScopePayload::disassemble(rd, self)?.into()), @@ -256,6 +258,13 @@ impl FernetTokenProvider { .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; data.assemble(&mut buf, self)?; } + Token::Trust(data) => { + write_array_len(&mut buf, 7) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + write_pfix(&mut buf, 3) + .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; + data.assemble(&mut buf, self)?; + } Token::FederationUnscoped(data) => { write_array_len(&mut buf, 8) .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?; @@ -770,4 +779,24 @@ pub(super) mod tests { let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap()); assert_eq!(token, dec_token); } + + #[tokio::test] + async fn test_trust_roundtrip() { + let token = Token::Trust(TrustPayload { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + trust_id: Uuid::new_v4().simple().to_string(), + project_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() + }); + + let mut provider = FernetTokenProvider::new(setup_config()); + provider.load_keys().unwrap(); + + let encrypted = provider.encrypt(&token).unwrap(); + let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap()); + assert_eq!(token, dec_token); + } } diff --git a/src/token/backend/fernet/trust.rs b/src/token/backend/fernet/trust.rs new file mode 100644 index 0000000..bf68294 --- /dev/null +++ b/src/token/backend/fernet/trust.rs @@ -0,0 +1,101 @@ +// 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 rmp::{decode::read_pfix, encode::write_pfix}; +use std::io::Write; + +use crate::token::{ + backend::fernet::{FernetTokenProvider, MsgPackToken, utils}, + error::TokenProviderError, + types::TrustPayload, +}; + +impl MsgPackToken for TrustPayload { + 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.project_id)?; + utils::write_time(wd, self.expires_at)?; + utils::write_audit_ids(wd, self.audit_ids.clone())?; + utils::write_uuid(wd, &self.trust_id)?; + + 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 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 trust_id = utils::read_uuid(rd)?; + + Ok(Self { + user_id, + methods, + expires_at, + audit_ids, + project_id, + trust_id, + ..Default::default() + }) + } +} + +#[cfg(test)] +mod tests { + use chrono::{Local, SubsecRound}; + use uuid::Uuid; + + use super::*; + use crate::token::tests::setup_config; + + #[test] + fn test_roundtrip() { + let token = TrustPayload { + user_id: Uuid::new_v4().simple().to_string(), + methods: vec!["password".into()], + project_id: Uuid::new_v4().simple().to_string(), + trust_id: Uuid::new_v4().simple().to_string(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: Local::now().trunc_subsecs(0).into(), + ..Default::default() + }; + + let provider = FernetTokenProvider::new(setup_config()); + + let mut buf = vec![]; + token.assemble(&mut buf, &provider).unwrap(); + let encoded_buf = buf.clone(); + let decoded = TrustPayload::disassemble(&mut encoded_buf.as_slice(), &provider).unwrap(); + assert_eq!(token, decoded); + } +} diff --git a/src/token/mod.rs b/src/token/mod.rs index e1ed6b6..fd2f7b6 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -325,6 +325,10 @@ impl TokenProvider { Token::Restricted(data) => { data.user = user; } + Token::Trust(data) => { + // TODO: This maybe wrong for trust + data.user = user; + } } } Ok(()) diff --git a/src/token/types.rs b/src/token/types.rs index 1cde408..826d31d 100644 --- a/src/token/types.rs +++ b/src/token/types.rs @@ -30,6 +30,7 @@ pub mod federation_unscoped; pub mod project_scoped; pub mod provider_api; pub mod restricted; +pub mod trust; pub mod unscoped; pub use application_credential::*; @@ -44,6 +45,7 @@ pub use federation_unscoped::{FederationUnscopedPayload, FederationUnscopedPaylo pub use project_scoped::{ProjectScopePayload, ProjectScopePayloadBuilder}; pub use provider_api::TokenApi; pub use restricted::*; +pub use trust::*; pub use unscoped::*; /// Fernet Token. @@ -64,6 +66,8 @@ pub enum Token { ProjectScope(ProjectScopePayload), /// Restricted. Restricted(RestrictedPayload), + /// Trust. + Trust(TrustPayload), /// Unscoped. Unscoped(UnscopedPayload), } @@ -78,6 +82,7 @@ impl Token { Self::FederationDomainScope(x) => &x.user_id, Self::ProjectScope(x) => &x.user_id, Self::Restricted(x) => &x.user_id, + Self::Trust(x) => &x.user_id, Self::Unscoped(x) => &x.user_id, } } @@ -91,6 +96,7 @@ impl Token { Self::FederationDomainScope(x) => &x.user, Self::ProjectScope(x) => &x.user, Self::Restricted(x) => &x.user, + Self::Trust(x) => &x.user, Self::Unscoped(x) => &x.user, } } @@ -108,6 +114,7 @@ impl Token { Self::FederationDomainScope(x) => x.issued_at = issued_at, Self::ProjectScope(x) => x.issued_at = issued_at, Self::Restricted(x) => x.issued_at = issued_at, + Self::Trust(x) => x.issued_at = issued_at, Self::Unscoped(x) => x.issued_at = issued_at, } self @@ -126,6 +133,7 @@ impl Token { Self::FederationDomainScope(x) => &x.issued_at, Self::ProjectScope(x) => &x.issued_at, Self::Restricted(x) => &x.issued_at, + Self::Trust(x) => &x.issued_at, Self::Unscoped(x) => &x.issued_at, } } @@ -140,6 +148,7 @@ impl Token { Self::FederationDomainScope(x) => &x.expires_at, Self::ProjectScope(x) => &x.expires_at, Self::Restricted(x) => &x.expires_at, + Self::Trust(x) => &x.expires_at, Self::Unscoped(x) => &x.expires_at, } } @@ -153,6 +162,7 @@ impl Token { Self::FederationDomainScope(x) => &x.methods, Self::ProjectScope(x) => &x.methods, Self::Restricted(x) => &x.methods, + Self::Trust(x) => &x.methods, Self::Unscoped(x) => &x.methods, } } @@ -166,6 +176,7 @@ impl Token { Self::FederationDomainScope(x) => &x.audit_ids, Self::ProjectScope(x) => &x.audit_ids, Self::Restricted(x) => &x.audit_ids, + Self::Trust(x) => &x.audit_ids, Self::Unscoped(x) => &x.audit_ids, } } @@ -176,6 +187,7 @@ impl Token { Self::ProjectScope(x) => x.project.as_ref(), Self::FederationProjectScope(x) => x.project.as_ref(), Self::Restricted(x) => x.project.as_ref(), + Self::Trust(x) => x.project.as_ref(), _ => None, } } @@ -186,6 +198,7 @@ impl Token { Self::FederationProjectScope(x) => Some(&x.project_id), Self::ProjectScope(x) => Some(&x.project_id), Self::Restricted(x) => Some(&x.project_id), + Self::Trust(x) => Some(&x.project_id), _ => None, } } @@ -209,6 +222,7 @@ impl Token { Self::FederationDomainScope(x) => x.roles.as_ref(), Self::ProjectScope(x) => x.roles.as_ref(), Self::Restricted(x) => x.roles.as_ref(), + Self::Trust(x) => x.roles.as_ref(), _ => None, } } @@ -224,6 +238,7 @@ impl Validate for Token { Self::FederationDomainScope(x) => x.validate(), Self::ProjectScope(x) => x.validate(), Self::Restricted(x) => x.validate(), + Self::Trust(x) => x.validate(), Self::Unscoped(x) => x.validate(), } } diff --git a/src/token/types/trust.rs b/src/token/types/trust.rs new file mode 100644 index 0000000..9878ca4 --- /dev/null +++ b/src/token/types/trust.rs @@ -0,0 +1,98 @@ +// 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 +//! Trust token types. + +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use serde::Serialize; +use validator::Validate; + +use crate::assignment::types::Role; +use crate::error::BuilderError; +use crate::identity::types::UserResponse; +use crate::resource::types::Project; +use crate::token::types::{Token, common}; +use crate::trust::types::Trust; + +/// Trust token payload. +#[derive(Builder, Clone, Debug, Default, PartialEq, Serialize, Validate)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(into))] +pub struct TrustPayload { + /// User ID. + #[validate(length(min = 1, max = 64))] + pub user_id: String, + + /// Authentication methods used to obtain the token. + #[builder(default, setter(name = _methods))] + #[validate(length(min = 1))] + pub methods: Vec, + + /// Token audit IDs. + #[builder(default, setter(name = _audit_ids))] + #[validate(custom(function = "common::validate_audit_ids"))] + pub audit_ids: Vec, + + /// Token expiration datetime in UTC. + pub expires_at: DateTime, + + /// ID of the trust. + #[validate(length(min = 1, max = 64))] + pub trust_id: String, + + /// Project ID scope for the token. + #[validate(length(min = 1, max = 64))] + pub project_id: String, + + #[builder(default)] + pub issued_at: DateTime, + #[builder(default)] + pub user: Option, + #[builder(default)] + pub trust: Option, + #[builder(default)] + pub roles: Option>, + #[builder(default)] + pub project: Option, +} + +impl TrustPayloadBuilder { + 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: TrustPayload) -> Self { + Self::Trust(value) + } +} diff --git a/src/trust/backend.rs b/src/trust/backend.rs new file mode 100644 index 0000000..f4e5fa5 --- /dev/null +++ b/src/trust/backend.rs @@ -0,0 +1,50 @@ +// 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 +//! Trust provider Backend trait. +use async_trait::async_trait; +use dyn_clone::DynClone; + +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::trust::{TrustError, types::*}; + +pub mod error; +pub mod sql; + +pub use sql::SqlBackend; + +#[async_trait] +/// TrustBackend trait. +/// +/// Backend driver interface expected by the trust provider. +pub trait TrustBackend: DynClone + Send + Sync + std::fmt::Debug { + /// Set config + fn set_config(&mut self, config: Config); + + /// Get trust by ID. + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustError>; + + /// List trusts. + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustError>; +} + +dyn_clone::clone_trait_object!(TrustBackend); diff --git a/src/trust/backend/error.rs b/src/trust/backend/error.rs new file mode 100644 index 0000000..ead7a1f --- /dev/null +++ b/src/trust/backend/error.rs @@ -0,0 +1,55 @@ +// 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 thiserror::Error; + +use crate::assignment::backend::error::AssignmentDatabaseError; +use crate::error::{BuilderError, DatabaseError}; + +/// Database backend error for the database driver. +#[derive(Error, Debug)] +pub enum TrustDatabaseError { + /// Assignment database error. + #[error(transparent)] + AssignmentDatabase(#[from] AssignmentDatabaseError), + + /// Database error. + #[error(transparent)] + Database { + #[from] + source: DatabaseError, + }, + + /// DateTime parsing error. + #[error("error parsing int column as datetime: {expires_at}")] + ExpirationDateTimeParse { id: String, expires_at: i64 }, + + /// The trust has not been found. + #[error("{0}")] + TrustNotFound(String), + + #[error(transparent)] + Serde { + #[from] + source: serde_json::Error, + }, + + /// Structures builder error. + #[error(transparent)] + StructBuilder { + /// The source of the error. + #[from] + source: BuilderError, + }, +} diff --git a/src/trust/backend/sql.rs b/src/trust/backend/sql.rs new file mode 100644 index 0000000..d57f54a --- /dev/null +++ b/src/trust/backend/sql.rs @@ -0,0 +1,62 @@ +// 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 +//! Trust provider: database backend driver. + +use async_trait::async_trait; + +use super::TrustBackend; +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::trust::{TrustError, types::*}; + +mod trust; + +/// Sql Database revocation backend. +#[derive(Clone, Debug, Default)] +pub struct SqlBackend { + pub config: Config, +} + +impl SqlBackend {} + +#[async_trait] +impl TrustBackend for SqlBackend { + /// Set config. + fn set_config(&mut self, config: Config) { + self.config = config; + } + + /// Get trust by ID. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustError> { + Ok(trust::get(&state.db, id).await?) + } + + /// List trusts. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustError> { + Ok(trust::list(&state.db, params).await?) + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/trust/backend/sql/trust.rs b/src/trust/backend/sql/trust.rs new file mode 100644 index 0000000..9be2196 --- /dev/null +++ b/src/trust/backend/sql/trust.rs @@ -0,0 +1,114 @@ +// 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 crate::db::entity::trust as db_trust; +use crate::trust::backend::error::TrustDatabaseError; +use crate::trust::types::*; +use chrono::DateTime; +use serde_json::Value; +use tracing::error; + +mod get; +mod list; + +pub use get::get; +pub use list::list; + +impl TryFrom for Trust { + type Error = TrustDatabaseError; + + fn try_from(value: db_trust::Model) -> Result { + let mut builder = TrustBuilder::default(); + if let Some(val) = &value.deleted_at { + builder.deleted_at(val.and_utc()); + } + if let Some(val) = value.expires_at_int { + builder.expires_at( + DateTime::from_timestamp_micros(val) + .map(|val| val.to_utc()) + .ok_or_else(|| Self::Error::ExpirationDateTimeParse { + id: value.id.clone(), + expires_at: val, + })?, + ); + } else if let Some(val) = value.expires_at { + builder.expires_at(val.and_utc()); + } + if let Some(extra) = &value.extra + && extra != "{}" + { + match serde_json::from_str::(extra) { + Ok(extras) => { + builder.extra(extras); + } + Err(e) => { + error!("failed to deserialize trust extra: {e}"); + } + } + } + + builder.id(value.id); + builder.impersonation(value.impersonation); + if let Some(val) = &value.project_id { + builder.project_id(val); + } + if let Some(val) = value.remaining_uses { + builder.remaining_uses(val); + } + if let Some(val) = &value.redelegated_trust_id { + builder.redelegated_trust_id(val); + } + if let Some(val) = value.redelegation_count { + builder.redelegation_count(val); + } + builder.trustor_user_id(value.trustor_user_id); + builder.trustee_user_id(value.trustee_user_id); + Ok(builder.build()?) + } +} + +impl TryFrom<&db_trust::Model> for Trust { + type Error = TrustDatabaseError; + + fn try_from(value: &db_trust::Model) -> Result { + Self::try_from(value.clone()) + } +} + +#[cfg(test)] +mod tests { + + use crate::db::entity::trust; + + pub fn get_trust_mock, U1: AsRef, U2: AsRef>( + id: S, + trustor_id: U1, + trustee_id: U2, + ) -> trust::Model { + trust::Model { + id: id.as_ref().into(), + trustor_user_id: trustor_id.as_ref().into(), + trustee_user_id: trustee_id.as_ref().into(), + project_id: Some("pid".into()), + impersonation: false, + deleted_at: None, + expires_at: None, + remaining_uses: None, + extra: Some("{}".into()), + expires_at_int: None, + redelegated_trust_id: None, + redelegation_count: None, + } + } +} diff --git a/src/trust/backend/sql/trust/get.rs b/src/trust/backend/sql/trust/get.rs new file mode 100644 index 0000000..acf3aee --- /dev/null +++ b/src/trust/backend/sql/trust/get.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 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::assignment::types::Role; +use crate::db::entity::{ + prelude::{Role as DbRole, Trust as DbTrust}, + trust as db_trust, +}; +use crate::error::DbContextExt; +use crate::trust::backend::error::TrustDatabaseError; +use crate::trust::types::*; + +/// Get trust credential by the ID. +pub async fn get>( + db: &DatabaseConnection, + id: I, +) -> Result, TrustDatabaseError> { + if let Some(ref entry) = DbTrust::find() + .filter(db_trust::Column::Id.eq(id.as_ref())) + .one(db) + .await + .context("fetching trust by id")? + { + let roles = entry + .find_related(DbRole) + .all(db) + .await + .context("fetching trust roles")? + .into_iter() + .map(TryInto::::try_into) + .collect::, _>>()?; + + let mut res: Trust = entry.try_into()?; + if !roles.is_empty() { + res.roles = Some(roles); + } + return Ok(Some(res)); + } + Ok(None) +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use super::super::tests::*; + use super::*; + use crate::assignment::backend::sql::role::tests::get_role_mock; + + #[tokio::test] + async fn test_get() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_trust_mock("trust_id", "trustor", "trustee")]]) + .append_query_results([vec![get_role_mock("role_id")]]) + .into_connection(); + + assert_eq!( + get(&db, "trust_id").await.unwrap().unwrap(), + Trust { + id: "trust_id".into(), + trustor_user_id: "trustor".into(), + trustee_user_id: "trustee".into(), + project_id: Some("pid".into()), + impersonation: false, + roles: Some(vec![Role { + id: "role_id".into(), + domain_id: Some("foo_domain".into()), + name: "foo".to_owned(), + ..Default::default() + }]), + ..Default::default() + } + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "trust"."id", "trust"."trustor_user_id", "trust"."trustee_user_id", "trust"."project_id", "trust"."impersonation", "trust"."deleted_at", "trust"."expires_at", "trust"."remaining_uses", "trust"."extra", "trust"."expires_at_int", "trust"."redelegated_trust_id", "trust"."redelegation_count" FROM "trust" WHERE "trust"."id" = $1 LIMIT $2"#, + ["trust_id".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "role"."id", "role"."name", "role"."extra", "role"."domain_id", "role"."description" FROM "role" INNER JOIN "trust_role" ON "trust_role"."role_id" = "role"."id" INNER JOIN "trust" ON "trust"."id" = "trust_role"."trust_id" WHERE "trust"."id" = $1"#, + ["trust_id".into()] + ), + ] + ); + } +} diff --git a/src/trust/backend/sql/trust/list.rs b/src/trust/backend/sql/trust/list.rs new file mode 100644 index 0000000..b33ccae --- /dev/null +++ b/src/trust/backend/sql/trust/list.rs @@ -0,0 +1,162 @@ +// 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 +//! # List trusts +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; +use sea_orm::{Cursor, SelectModel}; + +use crate::assignment::types::Role; +use crate::db::entity::{ + prelude::{Role as DbRole, Trust as DbTrust, TrustRole as DbTrustRole}, + trust as db_trust, +}; +use crate::error::DbContextExt; +use crate::trust::backend::error::TrustDatabaseError; +use crate::trust::types::*; + +/// Prepare the paginated query for listing trusts. +fn get_list_query( + params: &TrustListParameters, +) -> Result>, TrustDatabaseError> { + let mut select = DbTrust::find(); + + if !params.include_deleted.is_some_and(|x| x) { + select = select.filter(db_trust::Column::DeletedAt.is_null()); + }; + + let mut cursor = select.cursor_by(db_trust::Column::Id); + if let Some(limit) = params.limit { + cursor.first(limit); + } + if let Some(marker) = ¶ms.marker { + cursor.after(marker); + } + Ok(cursor) +} + +/// List trusts. +pub async fn list( + db: &DatabaseConnection, + params: &TrustListParameters, +) -> Result, TrustDatabaseError> { + let db_trusts: Vec = get_list_query(params)? + .all(db) + .await + .context("listing trusts")?; + + let roles: Vec> = db_trusts + .load_many_to_many(DbRole, DbTrustRole, db) + .await + .context("fetching trust roles")? + .into_iter() + .map(|tr| { + tr.into_iter() + .map(TryInto::::try_into) + .collect::, _>>() + }) + .collect::>, _>>()?; + + db_trusts + .into_iter() + .zip(roles.into_iter()) + .map(|(trust, roles)| { + let mut res: Trust = trust.try_into()?; + if !roles.is_empty() { + res.roles = Some(roles); + } + Ok(res) + }) + .collect::, TrustDatabaseError>>() +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, QueryOrder, Transaction, sea_query::*}; + + use super::super::tests::get_trust_mock; + use crate::assignment::backend::sql::role::tests::get_role_mock; + use crate::db::entity::trust_role as db_trust_role; + + use super::*; + + #[tokio::test] + async fn test_query_all() { + assert_eq!( + r#"SELECT "trust"."id", "trust"."trustor_user_id", "trust"."trustee_user_id", "trust"."project_id", "trust"."impersonation", "trust"."deleted_at", "trust"."expires_at", "trust"."remaining_uses", "trust"."extra", "trust"."expires_at_int", "trust"."redelegated_trust_id", "trust"."redelegation_count" FROM "trust" WHERE "trust"."deleted_at" IS NULL"#, + QueryOrder::query(&mut get_list_query(&TrustListParameters::default()).unwrap()) + .to_string(PostgresQueryBuilder) + ); + } + + #[tokio::test] + async fn test_query_include_deleted() { + assert!( + !QueryOrder::query( + &mut get_list_query(&TrustListParameters { + include_deleted: Some(true), + ..Default::default() + }) + .unwrap() + ) + .to_string(PostgresQueryBuilder) + .contains("\"trust\".\"deleted\" IS NULL") + ); + } + + #[tokio::test] + async fn test_list_no_params() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_trust_mock("1", "trustor", "trustee")]]) + .append_query_results([vec![db_trust_role::Model { + trust_id: "1".into(), + role_id: "rid".into(), + }]]) + .append_query_results([vec![get_role_mock("1")]]) + .into_connection(); + assert_eq!( + list(&db, &TrustListParameters::default()).await.unwrap(), + vec![Trust { + id: "1".into(), + trustor_user_id: "trustor".into(), + trustee_user_id: "trustee".into(), + project_id: Some("pid".into()), + impersonation: false, + ..Default::default() + }] + ); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "trust"."id", "trust"."trustor_user_id", "trust"."trustee_user_id", "trust"."project_id", "trust"."impersonation", "trust"."deleted_at", "trust"."expires_at", "trust"."remaining_uses", "trust"."extra", "trust"."expires_at_int", "trust"."redelegated_trust_id", "trust"."redelegation_count" FROM "trust" WHERE "trust"."deleted_at" IS NULL ORDER BY "trust"."id" ASC"#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "trust_role"."trust_id", "trust_role"."role_id" FROM "trust_role" WHERE "trust_role"."trust_id" IN ($1)"#, + ["1".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "role"."id", "role"."name", "role"."extra", "role"."domain_id", "role"."description" FROM "role" WHERE "role"."id" IN ($1)"#, + ["rid".into()] + ), + ] + ); + } +} diff --git a/src/trust/error.rs b/src/trust/error.rs new file mode 100644 index 0000000..fd9f63e --- /dev/null +++ b/src/trust/error.rs @@ -0,0 +1,68 @@ +// 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 +//! # Trust Error +use thiserror::Error; + +use crate::trust::backend::error::TrustDatabaseError; + +/// Trust extension error. +#[derive(Error, Debug)] +pub enum TrustError { + /// Supported authentication error. + #[error(transparent)] + AuthenticationInfo { + /// The source of the error. + #[from] + source: crate::auth::AuthenticationError, + }, + + /// SQL backend error. + #[error(transparent)] + Backend { + /// The source of the error. + source: TrustDatabaseError, + }, + + /// Conflict. + #[error("conflict: {0}")] + Conflict(String), + + /// (de)serialization error. + #[error(transparent)] + Serde { + /// The source of the error. + #[from] + source: serde_json::Error, + }, + + /// Unsupported driver. + #[error("unsupported driver {0}")] + UnsupportedDriver(String), +} + +impl From for TrustError { + fn from(source: TrustDatabaseError) -> Self { + match source { + TrustDatabaseError::Database { source } => match source { + cfl @ crate::error::DatabaseError::Conflict { .. } => { + Self::Conflict(cfl.to_string()) + } + other => Self::Backend { + source: TrustDatabaseError::Database { source: other }, + }, + }, + _ => Self::Backend { source }, + } + } +} diff --git a/src/trust/mock.rs b/src/trust/mock.rs new file mode 100644 index 0000000..ecc067e --- /dev/null +++ b/src/trust/mock.rs @@ -0,0 +1,49 @@ +// 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 +//! Trust - internal mocking tools. +use async_trait::async_trait; +#[cfg(test)] +use mockall::mock; + +use crate::config::Config; +use crate::plugin_manager::PluginManager; +use crate::trust::{TrustApi, TrustError, types::*}; + +use crate::keystone::ServiceState; + +#[cfg(test)] +mock! { + pub TrustProvider { + pub fn new(cfg: &Config, plugin_manager: &PluginManager) -> Result; + } + + #[async_trait] + impl TrustApi for TrustProvider { + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustError>; + + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustError>; + } + + impl Clone for TrustProvider { + fn clone(&self) -> Self; + } +} diff --git a/src/trust/mod.rs b/src/trust/mod.rs new file mode 100644 index 0000000..ddb8680 --- /dev/null +++ b/src/trust/mod.rs @@ -0,0 +1,118 @@ +// 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 +//! # Trust provider. +//! +//! Trusts +//! +//! A trust represents a user’s (the trustor) authorization to delegate roles to +//! another user (the trustee), and optionally allow the trustee to impersonate +//! the trustor. After the trustor has created a trust, the trustee can specify +//! the trust’s id attribute as part of an authentication request to then create +//! a token representing the delegated authority of the trustor. +//! +//! The trust contains constraints on the delegated attributes. A token created +//! based on a trust will convey a subset of the trustor’s roles on the +//! specified project. Optionally, the trust may only be valid for a specified +//! time period, as defined by expires_at. If no expires_at is specified, then +//! the trust is valid until it is explicitly revoked. +//! +//! The impersonation flag allows the trustor to optionally delegate +//! impersonation abilities to the trustee. To services validating the token, +//! the trustee will appear as the trustor, although the token will also contain +//! the impersonation flag to indicate that this behavior is in effect. +//! +//! A project_id may not be specified without at least one role, and vice versa. +//! In other words, there is no way of implicitly delegating all roles to a +//! trustee, in order to prevent users accidentally creating trust that are much +//! more broad in scope than intended. A trust without a project_id or any +//! delegated roles is unscoped, and therefore does not represent authorization +//! on a specific resource. +//! +//! Trusts are immutable. If the trustee or trustor wishes to modify the +//! attributes of the trust, they should create a new trust and delete the old +//! trust. If a trust is deleted, any tokens generated based on the trust are +//! immediately revoked. +//! +//! If the trustor loses access to any delegated attributes, the trust becomes +//! immediately invalid and any tokens generated based on the trust are +//! immediately revoked. +//! +//! Trusts can also be chained, meaning, a trust can be created by using a trust +//! scoped token. + +use async_trait::async_trait; +// +pub mod backend; +pub mod error; +#[cfg(test)] +mod mock; +pub mod types; +// +use crate::config::Config; +use crate::keystone::ServiceState; +use crate::plugin_manager::PluginManager; +use backend::{SqlBackend, TrustBackend}; + +pub use error::TrustError; +#[cfg(test)] +pub use mock::MockTrustProvider; +pub use types::*; + +// +/// Revoke provider. +#[derive(Clone, Debug)] +pub struct TrustProvider { + /// Backend driver. + backend_driver: Box, +} +impl TrustProvider { + pub fn new(config: &Config, plugin_manager: &PluginManager) -> Result { + let mut backend_driver = + if let Some(driver) = plugin_manager.get_trust_backend(config.trust.driver.clone()) { + driver.clone() + } else { + match config.trust.driver.as_str() { + "sql" => Box::new(SqlBackend::default()), + _ => { + return Err(TrustError::UnsupportedDriver(config.trust.driver.clone())); + } + } + }; + backend_driver.set_config(config.clone()); + Ok(Self { backend_driver }) + } +} +// +#[async_trait] +impl TrustApi for TrustProvider { + /// Get trust by ID. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustError> { + self.backend_driver.get_trust(state, id).await + } + + /// List trusts. + #[tracing::instrument(level = "debug", skip(self, state))] + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustError> { + self.backend_driver.list_trusts(state, params).await + } +} diff --git a/src/trust/types.rs b/src/trust/types.rs new file mode 100644 index 0000000..46456f4 --- /dev/null +++ b/src/trust/types.rs @@ -0,0 +1,24 @@ +// 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 + +//! # Trust Extension types + +//use derive_builder::Builder; +//use serde::{Deserialize, Serialize}; + +mod provider; +mod trust; + +pub use provider::TrustApi; +pub use trust::*; diff --git a/src/trust/types/provider.rs b/src/trust/types/provider.rs new file mode 100644 index 0000000..92b84ff --- /dev/null +++ b/src/trust/types/provider.rs @@ -0,0 +1,37 @@ +// 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 + +//! # Trust provider interface +use async_trait::async_trait; + +use crate::keystone::ServiceState; +use crate::trust::{TrustError, types::*}; + +/// Trust extension provider interface. +#[async_trait] +pub trait TrustApi: Send + Sync + Clone { + /// Get trust by ID. + async fn get_trust<'a>( + &self, + state: &ServiceState, + id: &'a str, + ) -> Result, TrustError>; + + /// List trusts. + async fn list_trusts( + &self, + state: &ServiceState, + params: &TrustListParameters, + ) -> Result, TrustError>; +} diff --git a/src/trust/types/trust.rs b/src/trust/types/trust.rs new file mode 100644 index 0000000..b4d424b --- /dev/null +++ b/src/trust/types/trust.rs @@ -0,0 +1,138 @@ +// 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 + +//! # Trust types +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use validator::Validate; + +use crate::assignment::types::Role; +use crate::error::BuilderError; + +/// A trust object. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Validate)] +#[builder(build_fn(error = "BuilderError"))] +#[builder(setter(strip_option, into))] +pub struct Trust { + /// The trust deletion date. + #[builder(default)] + pub deleted_at: Option>, + + /// Specifies the expiration time of the trust. A trust may be revoked ahead + /// of expiration. If the value represents a time in the past, the trust is + /// deactivated. In the redelegation case it must not exceed the value of + /// the corresponding `expires_at` field of the redelegated trust or it may + /// be omitted, then the `expires_at` value is copied from the + /// redelegated trust. + #[builder(default)] + pub expires_at: Option>, + + #[builder(default)] + pub extra: Option, + + /// The ID of the trust. + #[validate(length(min = 1, max = 64))] + pub id: String, + + /// If set to `true`, then the user attribute of tokens generated based on + /// the trust will represent that of the `trustor` rather than the + /// `trustee`, thus allowing the `trustee` to impersonate the `trustor`. + /// If impersonation is set to `false`, then the token’s user attribute + /// will represent that of the `trustee`. + pub impersonation: bool, + + /// Identifies the project upon which the trustor is delegating + /// authorization. + #[builder(default)] + #[serde(default)] + #[validate(length(min = 1, max = 64))] + pub project_id: Option, + + /// Specifies how many times the trust can be used to obtain a token. This + /// value is decreased each time a token is issued through the trust. Once + /// it reaches 0, no further tokens will be issued through the trust. The + /// default value is null, meaning there is no limit on the number of tokens + /// issued through the trust. If redelegation is enabled it must not be set. + #[builder(default)] + pub remaining_uses: Option, + + /// Returned with redelegated trust provides information about the + /// predecessor in the trust chain. + #[builder(default)] + #[validate(length(min = 1, max = 64))] + pub redelegated_trust_id: Option, + + /// Specifies the maximum remaining depth of the redelegated trust chain. + /// Each subsequent trust has this field decremented by 1 automatically. The + /// initial trustor issuing new trust that can be redelegated, must set + /// allow_redelegation to true and may set `redelegation_count` to an + /// integer value less than or equal to `max_redelegation_count` + /// configuration parameter in order to limit the possible length of + /// derived trust chains. The trust issued by the `trustor` using a + /// project-scoped token (not redelegating), in which + /// `allow_redelegation` is set to true (the new + /// trust is redelegatable), will be populated with the value specified in + /// the `max_redelegation_count` configuration parameter if + /// `redelegation_count` is not set or set to `null`. If + /// `allow_redelegation` is set to `false` then `redelegation_count` + /// will be set to 0 in the trust. If the trust is being issued by the + /// `trustee` of a redelegatable trust-scoped token (redelegation case) + /// then `redelegation_count` should not be set, as it + /// will automatically be set to the value in the redelegatable + /// trust-scoped token decremented by 1. Note, if the resulting value is + /// 0, this means that the new trust will not be redelegatable, + /// regardless of the value of `allow_redelegation`. + #[builder(default)] + pub redelegation_count: Option, + + /// Specifies the subset of the trustor's roles on the `project_id` to be + /// granted to the `trustee` when the token is consumed. The trustor must + /// already be granted these roles in the project referenced by the + /// `project_id` attribute. If redelegation is used (when trust-scoped token + /// is used and consumed trust has `allow_redelegation` set to true) this + /// parameter should contain redelegated trust's roles only. + /// Roles are only provided when the trust is created, and are subsequently + /// available as a separate read-only collection. Each role can be specified + /// by either id or name. + #[builder(default)] + pub roles: Option>, + + /// Represents the user who created the trust, and who’s authorization is + /// being delegated. + #[validate(length(min = 1, max = 64))] + pub trustor_user_id: String, + + /// Represents the user who is capable of consuming the trust. + #[validate(length(min = 1, max = 64))] + pub trustee_user_id: String, +} + +/// A trust list parameters. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Validate)] +#[builder(setter(strip_option, into))] +pub struct TrustListParameters { + /// Whether to include deleted trusts. + #[builder(default)] + pub include_deleted: Option, + + /// Limit number of entries on the single response page. + #[builder(default)] + pub limit: Option, + + /// Page marker (id of the last entry on the previous page. + #[builder(default)] + pub marker: Option, +}