From 4ef017390eb791e83d1c7931f11edc12c8b61274 Mon Sep 17 00:00:00 2001 From: konac-hamza Date: Fri, 20 Feb 2026 00:46:39 +0300 Subject: [PATCH 1/3] feat: Integration test for verifying grant revocation --- tests/integration/src/assignment/grant.rs | 32 ++-- .../integration/src/assignment/grant/list.rs | 6 +- .../src/assignment/grant/revoke.rs | 154 +++++++++++++++++- 3 files changed, 171 insertions(+), 21 deletions(-) diff --git a/tests/integration/src/assignment/grant.rs b/tests/integration/src/assignment/grant.rs index 946ecec7..7735d8d9 100644 --- a/tests/integration/src/assignment/grant.rs +++ b/tests/integration/src/assignment/grant.rs @@ -16,15 +16,17 @@ mod list; mod revoke; use eyre::Report; -use sea_orm::{DbConn, entity::*}; -use std::sync::Arc; - +use openstack_keystone::assignment::AssignmentApi; +use openstack_keystone::assignment::types::AssignmentCreate; use openstack_keystone::config::Config; use openstack_keystone::db::entity::{prelude::*, project}; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; use openstack_keystone::policy::PolicyFactory; use openstack_keystone::provider::Provider; +use sea_orm::{DbConn, entity::*}; +use std::sync::Arc; +use tempfile::TempDir; //use super::setup_schema; use crate::common::{bootstrap, get_isolated_database}; @@ -70,18 +72,26 @@ async fn setup_assignment_data(db: &DbConn) -> Result<(), Report> { Ok(()) } -async fn get_state() -> Result, Report> { +async fn get_state() -> Result<(Arc, TempDir), Report> { let db = get_isolated_database().await?; setup_assignment_data(&db).await?; - let cfg: Config = Config::default(); + let tmp_fernet_repo = TempDir::new()?; + + let mut cfg: Config = Config::default(); + cfg.auth.methods = vec!["application_credential".into(), "password".into()]; + cfg.fernet_tokens.key_repository = tmp_fernet_repo.path().to_path_buf(); + let fernet_utils = openstack_keystone::token::backend::fernet::utils::FernetUtils { + key_repository: cfg.fernet_tokens.key_repository.clone(), + max_active_keys: cfg.fernet_tokens.max_active_keys, + }; + fernet_utils.initialize_key_repository()?; let plugin_manager = PluginManager::default(); let provider = Provider::new(cfg.clone(), plugin_manager)?; - Ok(Arc::new(Service::new( - cfg, - db, - provider, - PolicyFactory::default(), - )?)) + + Ok(( + Arc::new(Service::new(cfg, db, provider, PolicyFactory::default())?), + tmp_fernet_repo, + )) } diff --git a/tests/integration/src/assignment/grant/list.rs b/tests/integration/src/assignment/grant/list.rs index f790cd22..497f9dae 100644 --- a/tests/integration/src/assignment/grant/list.rs +++ b/tests/integration/src/assignment/grant/list.rs @@ -73,7 +73,7 @@ async fn init_data(state: &ServiceState) -> Result<()> { #[traced_test] #[tokio::test] async fn test_list_user_domain() -> Result<()> { - let state = get_state().await?; + let (state, _) = get_state().await?; init_data(&state).await?; assert_eq!( @@ -119,7 +119,7 @@ async fn test_list_user_domain() -> Result<()> { #[tokio::test] async fn test_list_user_tl_project() -> Result<()> { - let state = get_state().await?; + let (state, _) = get_state().await?; init_data(&state).await?; assert_eq!( @@ -158,7 +158,7 @@ async fn test_list_user_tl_project() -> Result<()> { #[tokio::test] async fn test_list_user_sub_project() -> Result<()> { - let state = get_state().await?; + let (state, _) = get_state().await?; init_data(&state).await?; assert_eq!( diff --git a/tests/integration/src/assignment/grant/revoke.rs b/tests/integration/src/assignment/grant/revoke.rs index 156fc1a6..3229bd4f 100644 --- a/tests/integration/src/assignment/grant/revoke.rs +++ b/tests/integration/src/assignment/grant/revoke.rs @@ -14,15 +14,19 @@ //! Test role assignment revocation. +use super::get_state; +use crate::common::{create_role, create_user}; use eyre::Result; -use tracing_test::traced_test; - +use openstack_keystone::application_credential::ApplicationCredentialApi; +use openstack_keystone::application_credential::types::*; use openstack_keystone::assignment::{AssignmentApi, types::*}; +use openstack_keystone::auth::*; use openstack_keystone::keystone::ServiceState; - -use super::get_state; -use crate::common::create_role; - +use openstack_keystone::resource::types::ProjectBuilder; +use openstack_keystone::role::types::*; +use openstack_keystone::token::{TokenApi, TokenProviderError}; +use tracing_test::traced_test; +use uuid::Uuid; async fn grant_exists( state: &ServiceState, user_id: &str, @@ -61,7 +65,7 @@ async fn grant_exists( #[traced_test] #[tokio::test] async fn test_revoke_user_project_grant() -> Result<()> { - let state = get_state().await?; + let (state, _tmp) = get_state().await?; create_role(&state, "role_revoke_1").await?; // Create a direct grant @@ -95,3 +99,139 @@ async fn test_revoke_user_project_grant() -> Result<()> { Ok(()) } + +#[traced_test] +#[tokio::test] +async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { + let (state, _tmp) = get_state().await?; + + let user = create_user(&state, Some("user_a")).await?; + create_role(&state, "role_revoke_auth").await?; + + // Grant role to user on project + let grant = state + .provider + .get_assignment_provider() + .create_grant( + &state, + AssignmentCreate::user_project(&user.id, "project_a", "role_revoke_auth", false), + ) + .await?; + + assert!( + grant_exists(&state, &user.id, "project_a", "role_revoke_auth", true).await?, + "Grant should exist after creation" + ); + + // Create application credential and issue a token BEFORE revocation + let cred: ApplicationCredentialCreateResponse = state + .provider + .get_application_credential_provider() + .create_application_credential( + &state, + ApplicationCredentialCreate { + access_rules: None, + name: Uuid::new_v4().to_string(), + project_id: "project_a".into(), + roles: vec![Role { + id: "role_revoke_auth".into(), + name: "role_revoke_auth".into(), + ..Default::default() + }], + user_id: user.id.clone(), + ..Default::default() + }, + ) + .await?; + + let authz = AuthzInfo::Project( + ProjectBuilder::default() + .id(cred.project_id.clone()) + .name("project_a") + .domain_id("domain_a") + .enabled(true) + .build()?, + ); + + let pre_revoke_token = state.provider.get_token_provider().issue_token( + AuthenticatedInfoBuilder::default() + .application_credential(cred.clone()) + .user_id(user.id.clone()) + .user(user.clone()) + .methods(vec!["application_credential".into()]) + .build()?, + authz.clone(), + None, + )?; + let pre_revoke_encoded = state + .provider + .get_token_provider() + .encode_token(&pre_revoke_token)?; + + // Sanity check: token is valid before revocation + assert!( + state + .provider + .get_token_provider() + .validate_token(&state, &pre_revoke_encoded, None, None) + .await + .is_ok(), + "Token should be valid before revocation" + ); + + // --- Revoke the grant --- + state + .provider + .get_assignment_provider() + .revoke_grant(&state, grant) + .await?; + + // CHECK 1: listing roles no longer returns the revoked role + assert!( + !grant_exists(&state, &user.id, "project_a", "role_revoke_auth", true).await?, + "Grant should not exist after revocation" + ); + + // CHECK 2: new auth does not obtain the role + let post_revoke_token = state.provider.get_token_provider().issue_token( + AuthenticatedInfoBuilder::default() + .application_credential(cred.clone()) + .user_id(user.id.clone()) + .user(user.clone()) + .methods(vec!["application_credential".into()]) + .build()?, + authz, + None, + )?; + let post_revoke_encoded = state + .provider + .get_token_provider() + .encode_token(&post_revoke_token)?; + + assert!( + matches!( + state + .provider + .get_token_provider() + .validate_token(&state, &post_revoke_encoded, None, None) + .await, + Err(TokenProviderError::ActorHasNoRolesOnTarget) + ), + "New token after revocation should fail validation" + ); + + // CHECK 3: existing auth (issued before revocation) is no longer accepted + assert!( + matches!( + state + .provider + .get_token_provider() + .validate_token(&state, &pre_revoke_encoded, None, None) + .await, + Err(TokenProviderError::ActorHasNoRolesOnTarget) + ), + "Pre-revocation token should fail validation after grant is revoked" + ); + + Ok(()) +} From a1688d8b0abdcbb83bb67c9d547b7c78303a70c7 Mon Sep 17 00:00:00 2001 From: konac-hamza Date: Thu, 26 Feb 2026 02:44:11 +0300 Subject: [PATCH 2/3] feat: Update test to catch vulnerability Explain potential reason of the vulnerability --- tests/integration/src/assignment/grant.rs | 11 ++- .../src/assignment/grant/revoke.rs | 68 +++++++++++++------ 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/tests/integration/src/assignment/grant.rs b/tests/integration/src/assignment/grant.rs index 7735d8d9..0d4faa15 100644 --- a/tests/integration/src/assignment/grant.rs +++ b/tests/integration/src/assignment/grant.rs @@ -15,20 +15,19 @@ mod list; mod revoke; +use std::sync::Arc; + use eyre::Report; -use openstack_keystone::assignment::AssignmentApi; -use openstack_keystone::assignment::types::AssignmentCreate; +use sea_orm::{DbConn, entity::*}; +use tempfile::TempDir; + use openstack_keystone::config::Config; use openstack_keystone::db::entity::{prelude::*, project}; use openstack_keystone::keystone::Service; use openstack_keystone::plugin_manager::PluginManager; use openstack_keystone::policy::PolicyFactory; use openstack_keystone::provider::Provider; -use sea_orm::{DbConn, entity::*}; -use std::sync::Arc; -use tempfile::TempDir; -//use super::setup_schema; use crate::common::{bootstrap, get_isolated_database}; async fn setup_assignment_data(db: &DbConn) -> Result<(), Report> { diff --git a/tests/integration/src/assignment/grant/revoke.rs b/tests/integration/src/assignment/grant/revoke.rs index 3229bd4f..e4b7bb26 100644 --- a/tests/integration/src/assignment/grant/revoke.rs +++ b/tests/integration/src/assignment/grant/revoke.rs @@ -14,9 +14,10 @@ //! Test role assignment revocation. -use super::get_state; -use crate::common::{create_role, create_user}; use eyre::Result; +use tracing_test::traced_test; +use uuid::Uuid; + use openstack_keystone::application_credential::ApplicationCredentialApi; use openstack_keystone::application_credential::types::*; use openstack_keystone::assignment::{AssignmentApi, types::*}; @@ -25,8 +26,10 @@ use openstack_keystone::keystone::ServiceState; use openstack_keystone::resource::types::ProjectBuilder; use openstack_keystone::role::types::*; use openstack_keystone::token::{TokenApi, TokenProviderError}; -use tracing_test::traced_test; -use uuid::Uuid; + +use super::get_state; +use crate::common::{create_role, create_user}; + async fn grant_exists( state: &ServiceState, user_id: &str, @@ -106,9 +109,11 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { let (state, _tmp) = get_state().await?; let user = create_user(&state, Some("user_a")).await?; + // Create two roles: one that will be granted and revoked, and another to confirm that revocation is specific create_role(&state, "role_revoke_auth").await?; + create_role(&state, "role_exist_auth").await?; - // Grant role to user on project + // Grant first role that will be revoked let grant = state .provider .get_assignment_provider() @@ -123,6 +128,19 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { "Grant should exist after creation" ); + // Grant second role that will remain unaffected + let _ = state + .provider + .get_assignment_provider() + .create_grant( + &state, + AssignmentCreate::user_project(&user.id, "project_a", "role_exist_auth", false), + ) + .await?; + assert!( + grant_exists(&state, &user.id, "project_a", "role_exist_auth", true).await?, + "Grant should exist after creation" + ); // Create application credential and issue a token BEFORE revocation let cred: ApplicationCredentialCreateResponse = state .provider @@ -133,11 +151,18 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { access_rules: None, name: Uuid::new_v4().to_string(), project_id: "project_a".into(), - roles: vec![Role { - id: "role_revoke_auth".into(), - name: "role_revoke_auth".into(), - ..Default::default() - }], + roles: vec![ + Role { + id: "role_revoke_auth".into(), + name: "role_revoke_auth".into(), + ..Default::default() + }, + Role { + id: "role_exist_auth".into(), + name: "role_exist_auth".into(), + ..Default::default() + }, + ], user_id: user.id.clone(), ..Default::default() }, @@ -208,17 +233,16 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { .get_token_provider() .encode_token(&post_revoke_token)?; - assert!( - matches!( - state - .provider - .get_token_provider() - .validate_token(&state, &post_revoke_encoded, None, None) - .await, - Err(TokenProviderError::ActorHasNoRolesOnTarget) - ), - "New token after revocation should fail validation" - ); + let validated = state + .provider + .get_token_provider() + .validate_token(&state, &post_revoke_encoded, None, None) + .await?; + + let roles = validated.roles().expect("Token should have roles"); + + assert!(roles.iter().any(|r| r.id == "role_exist_auth")); + assert!(!roles.iter().any(|r| r.id == "role_revoke_auth")); // CHECK 3: existing auth (issued before revocation) is no longer accepted assert!( @@ -228,7 +252,7 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { .get_token_provider() .validate_token(&state, &pre_revoke_encoded, None, None) .await, - Err(TokenProviderError::ActorHasNoRolesOnTarget) + Err(TokenProviderError::TokenRevoked) ), "Pre-revocation token should fail validation after grant is revoked" ); From 8436997c45698459629b76ef930de0086ed13010 Mon Sep 17 00:00:00 2001 From: konac-hamza Date: Wed, 11 Mar 2026 00:23:32 +0300 Subject: [PATCH 3/3] fix: Fix the test by the new improvements --- .../integration/src/assignment/grant/revoke.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/integration/src/assignment/grant/revoke.rs b/tests/integration/src/assignment/grant/revoke.rs index e4b7bb26..7771a626 100644 --- a/tests/integration/src/assignment/grant/revoke.rs +++ b/tests/integration/src/assignment/grant/revoke.rs @@ -152,15 +152,15 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { name: Uuid::new_v4().to_string(), project_id: "project_a".into(), roles: vec![ - Role { + RoleRef { id: "role_revoke_auth".into(), - name: "role_revoke_auth".into(), - ..Default::default() + domain_id: None, + name: Some("role_exist_auth".into()), }, - Role { + RoleRef { id: "role_exist_auth".into(), - name: "role_exist_auth".into(), - ..Default::default() + name: Some("role_exist_auth".into()), + domain_id: None, }, ], user_id: user.id.clone(), @@ -203,6 +203,7 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { .is_ok(), "Token should be valid before revocation" ); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; // --- Revoke the grant --- state @@ -216,6 +217,7 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { !grant_exists(&state, &user.id, "project_a", "role_revoke_auth", true).await?, "Grant should not exist after revocation" ); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; // CHECK 2: new auth does not obtain the role let post_revoke_token = state.provider.get_token_provider().issue_token( @@ -239,7 +241,9 @@ async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { .validate_token(&state, &post_revoke_encoded, None, None) .await?; - let roles = validated.roles().expect("Token should have roles"); + let roles = validated + .effective_roles() + .expect("Token should have effective roles"); assert!(roles.iter().any(|r| r.id == "role_exist_auth")); assert!(!roles.iter().any(|r| r.id == "role_revoke_auth"));