Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions tests/integration/src/assignment/grant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment regarding the imports sorting is applicable here.

use openstack_keystone::config::Config;
use openstack_keystone::db::entity::{prelude::*, project};
Expand All @@ -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> {
Expand Down Expand Up @@ -70,18 +71,26 @@ async fn setup_assignment_data(db: &DbConn) -> Result<(), Report> {
Ok(())
}

async fn get_state() -> Result<Arc<Service>, Report> {
async fn get_state() -> Result<(Arc<Service>, 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,
))
}
6 changes: 3 additions & 3 deletions tests/integration/src/assignment/grant/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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!(
Expand Down
172 changes: 170 additions & 2 deletions tests/integration/src/assignment/grant/revoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@

use eyre::Result;
use tracing_test::traced_test;
use uuid::Uuid;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep the spaces between import groups. First group is typically std (+ whatever is std nowadays like tracing, eyre), second group are the other foreign crates, 3rd are the own crates (i.e. when a project spreads across multiple crates, what in our case is going to be openstack_keystone_api_types, openstack_keystone_storage, etc). The last group are the imports from the current crate itself.

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 openstack_keystone::resource::types::ProjectBuilder;
use openstack_keystone::role::types::*;
use openstack_keystone::token::{TokenApi, TokenProviderError};

use super::get_state;
use crate::common::create_role;
use crate::common::{create_role, create_user};

async fn grant_exists(
state: &ServiceState,
Expand Down Expand Up @@ -61,7 +68,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
Expand Down Expand Up @@ -95,3 +102,164 @@ 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 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 first role that will be revoked
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"
);

// 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
.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![
RoleRef {
id: "role_revoke_auth".into(),
domain_id: None,
name: Some("role_exist_auth".into()),
},
RoleRef {
id: "role_exist_auth".into(),
name: Some("role_exist_auth".into()),
domain_id: None,
},
],
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"
);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that one is not necessary


// --- 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"
);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a comment here, that since 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


// 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)?;

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_exist_auth"));
assert!(!roles.iter().any(|r| r.id == "role_revoke_auth"));

// CHECK 3: existing auth (issued before revocation) is no longer accepted
assert!(
matches!(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shows the real problem and is exactly the purpose of the test. The token must be considered as a revoked and not that the user is not having roles. In the combination with having more roles initially this would lead to the vulnerability.
I am not sure why exactly, but we should get the TokenProviderError::TokenRevoked error

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"
);

Ok(())
}
Loading