From aad50fa724fedf8f96c2b0e90e4be4c066675055 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 17 Nov 2025 19:04:05 +0000 Subject: [PATCH] feat: Add `revoke.revoke_token` provider method --- src/api/v4/auth/token/mod.rs | 18 ---- src/revoke/backend.rs | 14 ++- src/revoke/backend/sql.rs | 86 +++------------- src/revoke/backend/sql/create.rs | 150 +++++++++++++++++++++++++++ src/revoke/backend/sql/list.rs | 2 +- src/revoke/mock.rs | 6 ++ src/revoke/mod.rs | 11 ++ src/revoke/types.rs | 170 +++++++++++++++++++++++++++++++ 8 files changed, 364 insertions(+), 93 deletions(-) create mode 100644 src/revoke/backend/sql/create.rs diff --git a/src/api/v4/auth/token/mod.rs b/src/api/v4/auth/token/mod.rs index 9d14f224..d801a808 100644 --- a/src/api/v4/auth/token/mod.rs +++ b/src/api/v4/auth/token/mod.rs @@ -26,21 +26,3 @@ pub mod types; pub(super) fn openapi_router() -> OpenApiRouter { v3_token::openapi_router() } - -#[cfg(test)] -mod tests { - - use crate::policy::{MockPolicy, MockPolicyFactory, PolicyEvaluationResult}; - - fn get_policy_factory_mock() -> MockPolicyFactory { - let mut policy_factory_mock = MockPolicyFactory::default(); - policy_factory_mock.expect_instantiate().returning(|| { - let mut policy_mock = MockPolicy::default(); - policy_mock - .expect_enforce() - .returning(|_, _, _, _| Ok(PolicyEvaluationResult::allowed())); - Ok(policy_mock) - }); - policy_factory_mock - } -} diff --git a/src/revoke/backend.rs b/src/revoke/backend.rs index 75cb51a4..3a32919e 100644 --- a/src/revoke/backend.rs +++ b/src/revoke/backend.rs @@ -12,7 +12,7 @@ // // SPDX-License-Identifier: Apache-2.0 //! Token revocation: Backends. - +//! Revocation provider Backend trait. use async_trait::async_trait; use dyn_clone::DynClone; @@ -25,6 +25,9 @@ pub mod error; pub mod sql; #[async_trait] +/// RevokeBackend trait. +/// +/// Backend driver interface expected by the revocation provider. pub trait RevokeBackend: DynClone + Send + Sync + std::fmt::Debug { /// Set config fn set_config(&mut self, config: Config); @@ -37,6 +40,15 @@ pub trait RevokeBackend: DynClone + Send + Sync + std::fmt::Debug { state: &ServiceState, token: &Token, ) -> Result; + + /// Revoke the token. + /// + /// Mark the token as revoked to prohibit from being used even while not expired. + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError>; } dyn_clone::clone_trait_object!(RevokeBackend); diff --git a/src/revoke/backend/sql.rs b/src/revoke/backend/sql.rs index 48876c6f..3cef8714 100644 --- a/src/revoke/backend/sql.rs +++ b/src/revoke/backend/sql.rs @@ -14,9 +14,6 @@ //! Revoke provider: database backend. use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use derive_builder::Builder; -use serde::{Deserialize, Serialize}; use super::RevokeBackend; use crate::config::Config; @@ -24,8 +21,10 @@ use crate::db::entity::revocation_event as db_revocation_event; use crate::keystone::ServiceState; use crate::revoke::RevokeProviderError; use crate::revoke::backend::error::RevokeDatabaseError; +use crate::revoke::types::*; use crate::token::types::Token; +mod create; mod list; /// Sql Database revocation backend. @@ -79,77 +78,18 @@ impl RevokeBackend for SqlBackend { Ok(false) } } -} - -/// Revocation event. -#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] -#[builder(setter(strip_option, into))] -pub struct RevocationEvent { - pub domain_id: Option, - pub project_id: Option, - pub user_id: Option, - pub role_id: Option, - pub trust_id: Option, - pub consumer_id: Option, - pub access_token_id: Option, - pub issued_before: DateTime, - pub expires_at: Option>, - pub revoked_at: DateTime, - pub audit_id: Option, - pub audit_chain_id: Option, -} -/// Revocation list parameters. -/// -/// It may be necessary to list revocation events not related to the certain token. -#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] -#[builder(setter(strip_option, into))] -struct RevocationEventListParameters { - //pub access_token_id: Option, - //pub audit_chain_id: Option, - #[builder(default)] - pub audit_id: Option, - //pub consumer_id: Option, - #[builder(default)] - pub domain_id: Option, - #[builder(default)] - pub expires_at: Option>, - #[builder(default)] - pub issued_before: Option>, - #[builder(default)] - pub project_id: Option, - #[builder(default)] - pub revoked_at: Option>, - //pub role_id: Option, - //pub trust_id: Option, - #[builder(default)] - pub user_id: Option>, -} - -impl TryFrom<&Token> for RevocationEventListParameters { - type Error = RevokeProviderError; - fn try_from(value: &Token) -> Result { - // TODO: for trust token user_id can be trustee_id or trustor_id - Ok(Self { - //access_token_id: None, - //audit_chain_id: None, - audit_id: Some( - value - .audit_ids() - .first() - .ok_or_else(|| RevokeProviderError::TokenHasNoAuditId)?, - ) - .cloned(), - //consumer_id: None, - domain_id: value.domain().map(|domain| domain.id.clone()), - expires_at: None, - issued_before: Some(*value.issued_at()), - project_id: value.project_id().cloned(), - revoked_at: None, - //role_id: None, - //trust_id: None, - user_id: Some(vec![value.user_id().clone()]), - }) + /// Revoke the token. + /// + /// Mark the token as revoked to prohibit from being used even while not expired. + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError> { + Ok(create::create(&state.db, token.try_into()?) + .await + .map(|_| ())?) } } diff --git a/src/revoke/backend/sql/create.rs b/src/revoke/backend/sql/create.rs new file mode 100644 index 00000000..00d80b24 --- /dev/null +++ b/src/revoke/backend/sql/create.rs @@ -0,0 +1,150 @@ +// 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 +//! Create a token revocation record. + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; + +use super::{RevocationEvent, RevocationEventCreate}; +use crate::db::entity::revocation_event as db_revocation_event; +use crate::revoke::backend::error::{RevokeDatabaseError, db_err}; + +/// Create token revocation record. +/// +/// Invalidate the token before the regular expiration. +pub async fn create( + db: &DatabaseConnection, + revocation: RevocationEventCreate, +) -> Result { + let entry = db_revocation_event::ActiveModel { + id: NotSet, + access_token_id: revocation + .access_token_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + audit_chain_id: revocation + .audit_chain_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + audit_id: revocation + .audit_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + consumer_id: revocation + .consumer_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + domain_id: revocation + .domain_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + expires_at: revocation + .expires_at + .map(|val| Set(Some(val.naive_utc()))) + .unwrap_or(NotSet), + issued_before: Set(revocation.issued_before.naive_utc()), + project_id: revocation + .project_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + revoked_at: Set(revocation.revoked_at.naive_utc()), + role_id: revocation.role_id.clone().map(Set).unwrap_or(NotSet).into(), + trust_id: revocation + .trust_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + user_id: revocation.user_id.clone().map(Set).unwrap_or(NotSet).into(), + }; + + let db_entry: db_revocation_event::Model = entry + .insert(db) + .await + .map_err(|err| db_err(err, "creating token revocation event"))?; + + db_entry.try_into() +} + +#[cfg(test)] +mod tests { + use chrono::{Days, Utc}; + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + use super::super::tests::get_mock; + use super::*; + + #[tokio::test] + async fn test_create() { + let time1 = Utc::now(); + let time2 = time1.checked_add_days(Days::new(1)).unwrap(); + let time3 = time2.checked_add_days(Days::new(1)).unwrap(); + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mock()]]) + .into_connection(); + + let req = RevocationEventCreate { + access_token_id: Some("access_token_id".into()), + audit_chain_id: Some("audit_chain_id".into()), + audit_id: Some("audit_id".into()), + consumer_id: Some("consumer_id".into()), + domain_id: Some("domain_id".into()), + expires_at: Some(time1), + issued_before: time2, + project_id: Some("project_id".into()), + revoked_at: time3, + role_id: Some("role_id".into()), + trust_id: Some("trust_id".into()), + user_id: Some("uid".into()), + }; + + create(&db, req).await.unwrap(); + + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "revocation_event" ("domain_id", "project_id", "user_id", "role_id", "trust_id", "consumer_id", "access_token_id", "issued_before", "expires_at", "revoked_at", "audit_id", "audit_chain_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING "id", "domain_id", "project_id", "user_id", "role_id", "trust_id", "consumer_id", "access_token_id", "issued_before", "expires_at", "revoked_at", "audit_id", "audit_chain_id""#, + [ + "domain_id".into(), + "project_id".into(), + "uid".into(), + "role_id".into(), + "trust_id".into(), + "consumer_id".into(), + "access_token_id".into(), + time2.naive_utc().into(), + time1.naive_utc().into(), + time3.naive_utc().into(), + "audit_id".into(), + "audit_chain_id".into() + ] + ),] + ); + } +} diff --git a/src/revoke/backend/sql/list.rs b/src/revoke/backend/sql/list.rs index eb1b6810..5296cea4 100644 --- a/src/revoke/backend/sql/list.rs +++ b/src/revoke/backend/sql/list.rs @@ -17,11 +17,11 @@ use sea_orm::DatabaseConnection; use sea_orm::entity::*; use sea_orm::query::*; -use super::{RevocationEvent, RevocationEventListParameters}; use crate::db::entity::{ prelude::RevocationEvent as DbRevocationEvent, revocation_event as db_revocation_event, }; use crate::revoke::backend::error::{RevokeDatabaseError, db_err}; +use crate::revoke::types::{RevocationEvent, RevocationEventListParameters}; fn build_query_filters( params: &RevocationEventListParameters, diff --git a/src/revoke/mock.rs b/src/revoke/mock.rs index 44b6af9a..7ecee160 100644 --- a/src/revoke/mock.rs +++ b/src/revoke/mock.rs @@ -38,6 +38,12 @@ mock! { state: &ServiceState, token: &Token, ) -> Result; + + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError>; } impl Clone for RevokeProvider { diff --git a/src/revoke/mod.rs b/src/revoke/mod.rs index b9402c37..551d74ad 100644 --- a/src/revoke/mod.rs +++ b/src/revoke/mod.rs @@ -99,4 +99,15 @@ impl RevokeApi for RevokeProvider { ) -> Result { self.backend_driver.is_token_revoked(state, token).await } + + /// Revoke the token. + /// + /// Mark the token as revoked to prohibit from being used even while not expired. + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError> { + self.backend_driver.revoke_token(state, token).await + } } diff --git a/src/revoke/types.rs b/src/revoke/types.rs index 603596a6..da9c08da 100644 --- a/src/revoke/types.rs +++ b/src/revoke/types.rs @@ -12,8 +12,12 @@ // // SPDX-License-Identifier: Apache-2.0 //! Token revocation types definitions. +//! Revocation provider types. use async_trait::async_trait; +use chrono::{DateTime, Timelike, Utc}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; use crate::keystone::ServiceState; use crate::revoke::RevokeProviderError; @@ -31,4 +35,170 @@ pub trait RevokeApi: Send + Sync + Clone { state: &ServiceState, token: &Token, ) -> Result; + + /// Revoke the token. + /// + /// Mark the token as revoked to prohibit from being used even while not expired. + async fn revoke_token( + &self, + state: &ServiceState, + token: &Token, + ) -> Result<(), RevokeProviderError>; +} + +/// Revocation event. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(strip_option, into))] +pub struct RevocationEvent { + pub domain_id: Option, + pub project_id: Option, + pub user_id: Option, + pub role_id: Option, + pub trust_id: Option, + pub consumer_id: Option, + pub access_token_id: Option, + pub issued_before: DateTime, + pub expires_at: Option>, + pub revoked_at: DateTime, + pub audit_id: Option, + pub audit_chain_id: Option, +} + +/// Revocation event creation data. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(strip_option, into))] +pub struct RevocationEventCreate { + pub domain_id: Option, + pub project_id: Option, + pub user_id: Option, + pub role_id: Option, + pub trust_id: Option, + pub consumer_id: Option, + pub access_token_id: Option, + pub issued_before: DateTime, + pub expires_at: Option>, + pub revoked_at: DateTime, + pub audit_id: Option, + pub audit_chain_id: Option, +} + +/// Revocation list parameters. +/// +/// It may be necessary to list revocation events not related to the certain token. +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(strip_option, into))] +pub struct RevocationEventListParameters { + //pub access_token_id: Option, + //pub audit_chain_id: Option, + #[builder(default)] + pub audit_id: Option, + //pub consumer_id: Option, + #[builder(default)] + pub domain_id: Option, + #[builder(default)] + pub expires_at: Option>, + #[builder(default)] + pub issued_before: Option>, + #[builder(default)] + pub project_id: Option, + #[builder(default)] + pub revoked_at: Option>, + //pub role_id: Option, + //pub trust_id: Option, + #[builder(default)] + pub user_id: Option>, +} + +impl TryFrom<&Token> for RevocationEventListParameters { + type Error = RevokeProviderError; + fn try_from(value: &Token) -> Result { + // TODO: for trust token user_id can be trustee_id or trustor_id + Ok(Self { + //access_token_id: None, + //audit_chain_id: None, + audit_id: Some( + value + .audit_ids() + .first() + .ok_or_else(|| RevokeProviderError::TokenHasNoAuditId)?, + ) + .cloned(), + //consumer_id: None, + domain_id: value.domain().map(|domain| domain.id.clone()), + expires_at: None, + issued_before: Some(*value.issued_at()), + project_id: value.project_id().cloned(), + revoked_at: None, + //role_id: None, + //trust_id: None, + user_id: Some(vec![value.user_id().clone()]), + }) + } +} + +impl TryFrom<&Token> for RevocationEventCreate { + type Error = RevokeProviderError; + fn try_from(value: &Token) -> Result { + let now = Utc::now(); + Ok(Self { + access_token_id: None, + audit_chain_id: None, + audit_id: Some( + value + .audit_ids() + .first() + .ok_or_else(|| RevokeProviderError::TokenHasNoAuditId)?, + ) + .cloned(), + consumer_id: None, + domain_id: None, + expires_at: value.expires_at().with_nanosecond(0), + issued_before: now, + project_id: None, + revoked_at: now, + role_id: None, + trust_id: None, + user_id: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::token::ProjectScopePayload; + + #[test] + fn test_create_from_token() { + let token = Token::ProjectScope(ProjectScopePayload { + user_id: "bar".into(), + methods: Vec::from(["password".to_string()]), + project_id: "pid".into(), + audit_ids: vec!["Zm9vCg".into()], + expires_at: DateTime::parse_from_rfc3339("2025-11-17T19:55:06.123456Z") + .unwrap() + .with_timezone(&Utc), + ..Default::default() + }); + let revocation: RevocationEventCreate = RevocationEventCreate::try_from(&token).unwrap(); + + assert!(revocation.access_token_id.is_none()); + assert!(revocation.audit_chain_id.is_none()); + assert_eq!( + *token.audit_ids().first().unwrap(), + revocation.audit_id.unwrap() + ); + assert!(revocation.consumer_id.is_none()); + assert!(revocation.domain_id.is_none()); + assert_eq!( + DateTime::parse_from_rfc3339("2025-11-17T19:55:06.000000Z") + .unwrap() + .with_timezone(&Utc), + revocation.expires_at.unwrap() + ); + assert!(revocation.project_id.is_none()); + assert!(revocation.role_id.is_none()); + assert!(revocation.trust_id.is_none()); + assert!(revocation.user_id.is_none()); + } }