-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Integration test for verifying grant revocation #573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,12 +16,19 @@ | |
|
|
||
| use eyre::Result; | ||
| use tracing_test::traced_test; | ||
| use uuid::Uuid; | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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 | ||
|
|
@@ -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; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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(()) | ||
| } | ||
There was a problem hiding this comment.
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.