diff --git a/crates/keystone/src/role/types/role.rs b/crates/keystone/src/role/types/role.rs index 84f51f81..ba3536d2 100644 --- a/crates/keystone/src/role/types/role.rs +++ b/crates/keystone/src/role/types/role.rs @@ -71,6 +71,16 @@ impl From for RoleRef { } } +impl From<&Role> for RoleRef { + fn from(value: &Role) -> Self { + Self { + id: value.id.clone(), + name: Some(value.name.clone()), + domain_id: value.domain_id.clone(), + } + } +} + /// Query parameters for listing roles. #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(build_fn(error = "BuilderError"))] diff --git a/tests/integration/src/assignment.rs b/tests/integration/src/assignment.rs index f99cb6dd..3eec3de4 100644 --- a/tests/integration/src/assignment.rs +++ b/tests/integration/src/assignment.rs @@ -14,7 +14,7 @@ use eyre::Result; use std::sync::Arc; -use openstack_keystone::assignment::{AssignmentApi, types::AssignmentCreate}; +use openstack_keystone::assignment::{AssignmentApi, types::*}; use openstack_keystone::keystone::Service; mod grant; @@ -35,3 +35,40 @@ pub async fn grant_role_to_user_on_project, P: Into, R: .await?; Ok(()) } + +pub async fn check_grant(state: &Arc, assignment: &Assignment) -> Result { + let mut params = RoleAssignmentListParametersBuilder::default(); + params.role_id(assignment.role_id.clone()); + match assignment.r#type { + AssignmentType::GroupDomain => { + params.domain_id(assignment.target_id.clone()); + params.group_id(assignment.actor_id.clone()); + } + AssignmentType::GroupProject => { + params.project_id(assignment.target_id.clone()); + params.group_id(assignment.actor_id.clone()); + } + AssignmentType::GroupSystem => { + params.system_id(assignment.target_id.clone()); + params.group_id(assignment.actor_id.clone()); + } + AssignmentType::UserDomain => { + params.domain_id(assignment.target_id.clone()); + params.user_id(assignment.actor_id.clone()); + } + AssignmentType::UserProject => { + params.project_id(assignment.target_id.clone()); + params.user_id(assignment.actor_id.clone()); + } + AssignmentType::UserSystem => { + params.system_id(assignment.target_id.clone()); + params.user_id(assignment.actor_id.clone()); + } + } + let assignments = state + .provider + .get_assignment_provider() + .list_role_assignments(state, ¶ms.build()?) + .await?; + Ok(assignments.len() > 0) +} diff --git a/tests/integration/src/assignment/grant.rs b/tests/integration/src/assignment/grant.rs index 946ecec7..0d4faa15 100644 --- a/tests/integration/src/assignment/grant.rs +++ b/tests/integration/src/assignment/grant.rs @@ -15,9 +15,11 @@ mod list; mod revoke; +use std::sync::Arc; + use eyre::Report; use sea_orm::{DbConn, entity::*}; -use std::sync::Arc; +use tempfile::TempDir; use openstack_keystone::config::Config; use openstack_keystone::db::entity::{prelude::*, project}; @@ -26,7 +28,6 @@ use openstack_keystone::plugin_manager::PluginManager; use openstack_keystone::policy::PolicyFactory; use openstack_keystone::provider::Provider; -//use super::setup_schema; use crate::common::{bootstrap, get_isolated_database}; async fn setup_assignment_data(db: &DbConn) -> Result<(), Report> { @@ -70,18 +71,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..3e202105 100644 --- a/tests/integration/src/assignment/grant/revoke.rs +++ b/tests/integration/src/assignment/grant/revoke.rs @@ -15,83 +15,254 @@ //! Test role assignment revocation. use eyre::Result; +use std::ops::Deref; use tracing_test::traced_test; +use uuid::Uuid; +use openstack_keystone::application_credential::{ApplicationCredentialApi, types::*}; use openstack_keystone::assignment::{AssignmentApi, types::*}; -use openstack_keystone::keystone::ServiceState; - -use super::get_state; -use crate::common::create_role; - -async fn grant_exists( - state: &ServiceState, - user_id: &str, - target_id: &str, - role_id: &str, - is_project: bool, -) -> Result { - // Build the query parameters based on whether it's project or domain - let params = if is_project { - RoleAssignmentListParametersBuilder::default() - .user_id(user_id) - .role_id(role_id) - .project_id(target_id) - .effective(false) - .build()? - } else { - RoleAssignmentListParametersBuilder::default() - .user_id(user_id) - .role_id(role_id) - .domain_id(target_id) - .effective(false) - .build()? - }; - - let assignments = state +use openstack_keystone::auth::*; +use openstack_keystone::identity::types::*; +use openstack_keystone::resource::types::*; +use openstack_keystone::role::types::*; +use openstack_keystone::token::{TokenApi, TokenProviderError}; + +use crate::assignment::{check_grant, grant_role_to_user_on_project}; +use crate::common::get_state; +use crate::identity::create_user; +use crate::resource::{create_domain, create_project}; +use crate::role::create_role; + +#[traced_test] +#[tokio::test] +async fn test_revoke_user_project_grant() -> Result<()> { + let (state, _tmp) = get_state().await?; + + let domain = create_domain( + &state, + DomainCreateBuilder::default() + .name(Uuid::new_v4().simple().to_string()) + .enabled(true) + .build()?, + ) + .await?; + let project = create_project( + &state, + ProjectCreateBuilder::default() + .name(Uuid::new_v4().simple().to_string()) + .domain_id(domain.id.clone()) + .enabled(true) + .build()?, + ) + .await?; + let user = create_user( + &state, + UserCreateBuilder::default() + .name("user_a") + .domain_id(domain.id.clone()) + .build()?, + ) + .await?; + let role_a = create_role(&state, RoleCreateBuilder::default().name("role_a").build()?).await?; + let role_b = create_role(&state, RoleCreateBuilder::default().name("role_b").build()?).await?; + grant_role_to_user_on_project(&state, &user.id, &project.id, &role_a.id).await?; + grant_role_to_user_on_project(&state, &user.id, &project.id, &role_b.id).await?; + + let assignment = AssignmentBuilder::default() + .actor_id(user.id.clone()) + .target_id(project.id.clone()) + .role_id(role_b.id.clone()) + .r#type(AssignmentType::UserProject) + .build()?; + + assert!( + check_grant(&state, &assignment).await?, + "Grant should exist" + ); + + // Revoke the grant + state .provider .get_assignment_provider() - .list_role_assignments(state, ¶ms) + .revoke_grant(&state, assignment.clone()) .await?; - Ok(assignments.iter().any(|a| { - a.role_id == role_id && a.actor_id == user_id && a.target_id == target_id && !a.inherited - })) + // Verify grant no longer exists + assert!( + !check_grant(&state, &assignment).await?, + "Grant should not exist after revocation" + ); + + Ok(()) } #[traced_test] #[tokio::test] -async fn test_revoke_user_project_grant() -> Result<()> { - let state = get_state().await?; - create_role(&state, "role_revoke_1").await?; +async fn test_revoke_user_project_grant_auth_impact() -> Result<()> { + let (state, _tmp) = get_state().await?; + + let domain = create_domain( + &state, + DomainCreateBuilder::default() + .name(Uuid::new_v4().simple().to_string()) + .enabled(true) + .build()?, + ) + .await?; + let project = create_project( + &state, + ProjectCreateBuilder::default() + .name(Uuid::new_v4().simple().to_string()) + .domain_id(domain.id.clone()) + .enabled(true) + .build()?, + ) + .await?; + let user = create_user( + &state, + UserCreateBuilder::default() + .name("user_a") + .domain_id(domain.id.clone()) + .build()?, + ) + .await?; + // Create two roles: one that will be granted and revoked, and another to confirm that revocation is specific + let role_a = create_role(&state, RoleCreateBuilder::default().name("role_a").build()?).await?; + let role_b = create_role(&state, RoleCreateBuilder::default().name("role_b").build()?).await?; + // Grant first role that will be revoked + grant_role_to_user_on_project(&state, &user.id, &project.id, &role_a.id).await?; + // Grant second role that will remain unaffected + grant_role_to_user_on_project(&state, &user.id, &project.id, &role_b.id).await?; - // Create a direct grant - let grant = state + let assignment_a = AssignmentBuilder::default() + .actor_id(user.id.clone()) + .target_id(project.id.clone()) + .role_id(role_a.id.clone()) + .r#type(AssignmentType::UserProject) + .build()?; + let assignment_b = AssignmentBuilder::default() + .actor_id(user.id.clone()) + .target_id(project.id.clone()) + .role_id(role_b.id.clone()) + .r#type(AssignmentType::UserProject) + .build()?; + + assert!( + check_grant(&state, &assignment_a).await?, + "Grant should exist" + ); + assert!( + check_grant(&state, &assignment_b).await?, + "Grant should exist" + ); + + // Create application credential and issue a token BEFORE revocation + let cred: ApplicationCredentialCreateResponse = state .provider - .get_assignment_provider() - .create_grant( + .get_application_credential_provider() + .create_application_credential( &state, - AssignmentCreate::user_project("user_a", "project_a", "role_revoke_1", false), + ApplicationCredentialCreate { + access_rules: None, + name: Uuid::new_v4().to_string(), + project_id: project.id.clone(), + roles: vec![RoleRef::from(role_a.deref()), RoleRef::from(role_b.deref())], + user_id: user.id.clone(), + ..Default::default() + }, ) .await?; - // Verify grant exists + let authz = AuthzInfo::Project( + ProjectBuilder::default() + .id(cred.project_id.clone()) + .name(project.id.clone()) + .domain_id(project.domain_id.clone()) + .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!( - grant_exists(&state, "user_a", "project_a", "role_revoke_1", true).await?, - "Grant should exist after creation" + 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 + // --- Revoke the grant --- state .provider .get_assignment_provider() - .revoke_grant(&state, grant) + .revoke_grant(&state, assignment_a.clone()) .await?; - - // Verify grant no longer exists + // CHECK 1: listing roles no longer returns the revoked role assert!( - !grant_exists(&state, "user_a", "project_a", "role_revoke_1", true).await?, + !check_grant(&state, &assignment_a).await?, "Grant should not exist after revocation" ); + // CHECK 2: 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::TokenRevoked) + ), + "Pre-revocation token should fail validation after grant is revoked" + ); + + // CHECK 3: new auth does not obtain the role + // token revocation is working with a seconds precision we need to wait for a new second before granting new token to prevent it being also eventually revoked + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let post_revoke_token = state.provider.get_token_provider().issue_token( + AuthenticatedInfoBuilder::default() + .application_credential(cred.clone()) + .user_id(user.id.clone()) + .user(user.clone()) + .methods(vec!["application_credential".into()]) + .build()?, + authz, + None, + )?; + let post_revoke_encoded = state + .provider + .get_token_provider() + .encode_token(&post_revoke_token)?; + + let validated = state + .provider + .get_token_provider() + .validate_token(&state, &post_revoke_encoded, None, None) + .await?; + + let roles = validated + .effective_roles() + .expect("Token should have effective roles"); + + assert!(roles.iter().any(|r| r.id == role_b.id)); + assert!(!roles.iter().any(|r| r.id == role_a.id)); Ok(()) } diff --git a/tests/integration/src/common.rs b/tests/integration/src/common.rs index 1ccbb90d..7e828308 100644 --- a/tests/integration/src/common.rs +++ b/tests/integration/src/common.rs @@ -236,7 +236,7 @@ pub async fn get_state() -> Result<(Arc, TempDir)> { let tmp_fernet_repo = TempDir::new()?; let mut cfg: Config = Config::default(); - cfg.auth.methods = vec!["password".into()]; + 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(),