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
9 changes: 9 additions & 0 deletions crates/core/src/assignment/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::assignment::AssignmentApi;
use crate::assignment::AssignmentProviderError;
use crate::assignment::types::*;
use crate::keystone::ServiceState;
use crate::role::types::*;

mock! {
pub AssignmentProvider {}
Expand All @@ -41,5 +42,13 @@ mock! {
state: &ServiceState,
params: Assignment,
) -> Result<(), AssignmentProviderError>;

// List user roles on project
async fn list_user_roles_on_project(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It is definitely not going to be efficient to build a dedicated function for every possible request combination. The provider already accepts a structure supporting a variety of queries that should be used for calls like this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

you are right but I have tried to prevent unnecessary iteration. Respect of the official document this api should return role list instead of assignment list. In order to achieve that response of the role_assignment method, we need to iterate all assignments and convert them to role. The implemented new method idea is prevent this inefficient iteration.

&self,
state: &ServiceState,
params: &RoleAssignmentListParameters,
) -> Result<Vec<Role>, AssignmentProviderError>;

}
}
14 changes: 14 additions & 0 deletions crates/core/src/assignment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use openstack_keystone_config::Config;
use crate::assignment::service::AssignmentService;
use crate::keystone::ServiceState;
use crate::plugin_manager::PluginManagerApi;
use crate::role::types::Role;
use types::*;

pub use error::AssignmentProviderError;
Expand Down Expand Up @@ -104,6 +105,19 @@ impl AssignmentApi for AssignmentProvider {
}
}

// List user roles on project
#[tracing::instrument(level = "info", skip(self, state))]
async fn list_user_roles_on_project(
&self,
state: &ServiceState,
params: &RoleAssignmentListParameters,
) -> Result<Vec<Role>, AssignmentProviderError> {
match self {
Self::Service(provider) => provider.list_user_roles_on_project(state, params).await,
#[cfg(any(test, feature = "mock"))]
Self::Mock(provider) => provider.list_user_roles_on_project(state, params).await,
}
}
/// Revoke grant
#[tracing::instrument(level = "info", skip(self, state))]
async fn revoke_grant(
Expand Down
206 changes: 206 additions & 0 deletions crates/core/src/assignment/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,44 @@ impl AssignmentApi for AssignmentService {
Ok(assignments)
}

/// List user roles on project
#[tracing::instrument(level = "info", skip(self, state))]
async fn list_user_roles_on_project(
&self,
state: &ServiceState,
params: &RoleAssignmentListParameters,
) -> Result<Vec<Role>, AssignmentProviderError> {
// Get assignments for the user on project
let assignments = self.backend_driver.list_assignments(state, params).await?;

// If no assignments, return empty list
if assignments.is_empty() {
return Ok(Vec::new());
}

// Extract unique role IDs from assignments
let unique_role_ids: std::collections::BTreeSet<String> =
assignments.iter().map(|a| a.role_id.clone()).collect();

// Fetch all roles and build a map
let all_roles: std::collections::BTreeMap<String, Role> = state
.provider
.get_role_provider()
.list_roles(state, &RoleListParameters::default())
.await?
.into_iter()
.map(|role| (role.id.clone(), role))
.collect();

// Return only the roles that are in the assignments
let roles: Vec<Role> = unique_role_ids
.into_iter()
.filter_map(|role_id| all_roles.get(&role_id).cloned())
.collect();

Ok(roles)
}

/// Revoke grant
async fn revoke_grant(
&self,
Expand Down Expand Up @@ -290,4 +328,172 @@ mod tests {

assert!(provider.revoke_grant(&state, assignment).await.is_ok());
}

#[tokio::test]
async fn test_list_user_roles_on_project_no_roles() {
let mut role_mock = MockRoleProvider::default();
role_mock.expect_list_roles().returning(|_, _| Ok(vec![]));
let state = get_mocked_state(None, Some(Provider::mocked_builder().mock_role(role_mock)));

let mut backend = MockAssignmentBackend::default();
backend
.expect_list_assignments()
.returning(|_, _| Ok(vec![]));

let provider = AssignmentService {
backend_driver: Arc::new(backend),
};

let roles = provider
.list_user_roles_on_project(
&state,
&RoleAssignmentListParameters {
user_id: Some("user_id".into()),
project_id: Some("project_id".into()),
..Default::default()
},
)
.await
.unwrap();

assert_eq!(
roles.len(),
0,
"Should return empty list when no assignments"
);
}

#[tokio::test]
async fn test_list_user_roles_on_project_single_role() {
let mut role_mock = MockRoleProvider::default();
role_mock.expect_list_roles().returning(|_, _| {
Ok(vec![
RoleBuilder::default()
.id("role_id_1")
.name("role_name_1")
.build()
.unwrap(),
])
});
let state = get_mocked_state(None, Some(Provider::mocked_builder().mock_role(role_mock)));

let mut backend = MockAssignmentBackend::default();
backend
.expect_list_assignments()
.withf(|_, params: &RoleAssignmentListParameters| {
params.user_id == Some("user_id".into())
&& params.project_id == Some("project_id".into())
})
.returning(|_, _| {
Ok(vec![
AssignmentBuilder::default()
.actor_id("user_id")
.role_id("role_id_1")
.target_id("project_id")
.r#type(AssignmentType::UserProject)
.build()
.unwrap(),
])
});

let provider = AssignmentService {
backend_driver: Arc::new(backend),
};

let roles = provider
.list_user_roles_on_project(
&state,
&RoleAssignmentListParameters {
user_id: Some("user_id".into()),
project_id: Some("project_id".into()),
..Default::default()
},
)
.await
.unwrap();

assert_eq!(roles.len(), 1, "Should return one role");
assert_eq!(roles[0].id, "role_id_1");
assert_eq!(roles[0].name, "role_name_1");
}

#[tokio::test]
async fn test_list_user_roles_on_project_multiple_roles() {
let mut role_mock = MockRoleProvider::default();
role_mock.expect_list_roles().returning(|_, _| {
Ok(vec![
RoleBuilder::default()
.id("role_id_1")
.name("role_name_1")
.build()
.unwrap(),
RoleBuilder::default()
.id("role_id_2")
.name("role_name_2")
.build()
.unwrap(),
RoleBuilder::default()
.id("role_id_3")
.name("role_name_3")
.build()
.unwrap(),
])
});
let state = get_mocked_state(None, Some(Provider::mocked_builder().mock_role(role_mock)));

let mut backend = MockAssignmentBackend::default();
backend
.expect_list_assignments()
.withf(|_, params: &RoleAssignmentListParameters| {
params.user_id == Some("user_id".into())
&& params.project_id == Some("project_id".into())
})
.returning(|_, _| {
Ok(vec![
AssignmentBuilder::default()
.actor_id("user_id")
.role_id("role_id_1")
.target_id("project_id")
.r#type(AssignmentType::UserProject)
.build()
.unwrap(),
AssignmentBuilder::default()
.actor_id("user_id")
.role_id("role_id_2")
.target_id("project_id")
.r#type(AssignmentType::UserProject)
.build()
.unwrap(),
AssignmentBuilder::default()
.actor_id("user_id")
.role_id("role_id_3")
.target_id("project_id")
.r#type(AssignmentType::UserProject)
.build()
.unwrap(),
])
});

let provider = AssignmentService {
backend_driver: Arc::new(backend),
};

let roles = provider
.list_user_roles_on_project(
&state,
&RoleAssignmentListParameters {
user_id: Some("user_id".into()),
project_id: Some("project_id".into()),
..Default::default()
},
)
.await
.unwrap();

assert_eq!(roles.len(), 3, "Should return three roles");
let role_ids: Vec<String> = roles.iter().map(|r| r.id.clone()).collect();
assert!(role_ids.contains(&"role_id_1".to_string()));
assert!(role_ids.contains(&"role_id_2".to_string()));
assert!(role_ids.contains(&"role_id_3".to_string()));
}
}
8 changes: 8 additions & 0 deletions crates/core/src/assignment/types/provider_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use async_trait::async_trait;
use super::assignment::*;
use crate::assignment::AssignmentProviderError;
use crate::keystone::ServiceState;
use crate::role::types::Role;

/// The trait covering [`Role`](crate::role::types::Role) assignments between
/// `actors` and `objects`.
Expand Down Expand Up @@ -49,4 +50,11 @@ pub trait AssignmentApi: Send + Sync {
state: &ServiceState,
params: Assignment,
) -> Result<(), AssignmentProviderError>;

/// List user roles on project
async fn list_user_roles_on_project(
&self,
state: &ServiceState,
params: &RoleAssignmentListParameters,
) -> Result<Vec<Role>, AssignmentProviderError>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ use crate::keystone::ServiceState;

mod check;
mod grant;
mod list;
mod revoke;

pub(crate) fn openapi_router() -> OpenApiRouter<ServiceState> {
OpenApiRouter::new().routes(routes!(check::check, grant::grant, revoke::revoke))
OpenApiRouter::new().routes(routes!(
check::check,
grant::grant,
revoke::revoke,
list::list
))
}
Loading
Loading