From e51d76728bf9dd272000958ff676164f9c326356 Mon Sep 17 00:00:00 2001 From: Mohammed Date: Wed, 17 Jun 2026 15:48:40 +0300 Subject: [PATCH 1/2] feat: Filter user listing by type --- crates/api-types/src/v3/user.rs | 22 ++++++++++++++ crates/api-types/src/v3/user_conv.rs | 14 ++++++++- crates/keystone/src/api/v3/user/list.rs | 39 ++++++++++++++++++++++++- crates/keystone/src/api/v4/user/mod.rs | 35 ++++++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/crates/api-types/src/v3/user.rs b/crates/api-types/src/v3/user.rs index b515628fb..384358e18 100644 --- a/crates/api-types/src/v3/user.rs +++ b/crates/api-types/src/v3/user.rs @@ -309,6 +309,28 @@ pub struct UserListParameters { /// Filter users by the federated unique ID. #[cfg_attr(feature = "validate", validate(length(max = 64)))] pub unique_id: Option, + + /// Filter users by type (`local`, `federated`, `nonlocal`, `all`). + #[serde(rename = "type")] + pub user_type: Option, +} + +/// User type filter for listing users. +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "lowercase")] +pub enum UserType { + /// All users (default behavior). + All, + /// Federated users only. + Federated, + /// Local users only. + Local, + /// Non-local users only. + NonLocal, + /// Service account users only. + #[serde(rename = "service_account")] + ServiceAccount, } #[cfg(test)] diff --git a/crates/api-types/src/v3/user_conv.rs b/crates/api-types/src/v3/user_conv.rs index 0bb78e0a9..5e0db3456 100644 --- a/crates/api-types/src/v3/user_conv.rs +++ b/crates/api-types/src/v3/user_conv.rs @@ -111,13 +111,25 @@ impl From for api_types::FederationProtocol } } +impl From for provider_types::UserType { + fn from(value: api_types::UserType) -> Self { + match value { + api_types::UserType::All => Self::All, + api_types::UserType::Federated => Self::Federated, + api_types::UserType::Local => Self::Local, + api_types::UserType::NonLocal => Self::NonLocal, + api_types::UserType::ServiceAccount => Self::ServiceAccount, + } + } +} + impl From for provider_types::UserListParameters { fn from(value: api_types::UserListParameters) -> Self { Self { domain_id: value.domain_id, name: value.name, unique_id: value.unique_id, - ..Default::default() + user_type: value.user_type.map(Into::into), } } } diff --git a/crates/keystone/src/api/v3/user/list.rs b/crates/keystone/src/api/v3/user/list.rs index 67f015913..ebc45b172 100644 --- a/crates/keystone/src/api/v3/user/list.rs +++ b/crates/keystone/src/api/v3/user/list.rs @@ -75,7 +75,9 @@ mod tests { use tower::ServiceExt; // for `call`, `oneshot`, and `ready` use tower_http::trace::TraceLayer; - use openstack_keystone_core_types::identity::{UserListParameters, UserResponseBuilder}; + use openstack_keystone_core_types::identity::{ + UserListParameters, UserResponseBuilder, UserType, + }; use super::super::openapi_router; use crate::api::tests::{get_mocked_state, test_fixture_scoped}; @@ -187,6 +189,41 @@ mod tests { let _res: UserList = serde_json::from_slice(&body).unwrap(); } + #[tokio::test] + async fn test_list_qp_type() { + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_list_users() + .withf(|_, qp: &UserListParameters| qp.user_type == Some(UserType::Local)) + .returning(|_, _| Ok(Vec::new())); + + let vsc = test_fixture_scoped(); + let state = get_mocked_state( + Provider::mocked_builder().mock_identity(identity_mock), + true, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?type=local") + .extension(vsc) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + #[tokio::test] async fn test_list_unauth() { let state = get_mocked_state(Provider::mocked_builder(), false, None).await; diff --git a/crates/keystone/src/api/v4/user/mod.rs b/crates/keystone/src/api/v4/user/mod.rs index 0d77c850e..ae1235665 100644 --- a/crates/keystone/src/api/v4/user/mod.rs +++ b/crates/keystone/src/api/v4/user/mod.rs @@ -317,6 +317,41 @@ mod tests { let _res: UserList = serde_json::from_slice(&body).unwrap(); } + #[tokio::test] + async fn test_list_qp_type() { + let vsc = test_fixture_scoped(); + let mut identity_mock = MockIdentityProvider::default(); + identity_mock + .expect_list_users() + .withf(|_, qp: &UserListParameters| qp.user_type == Some(UserType::Local)) + .returning(|_, _| Ok(Vec::new())); + + let state = get_mocked_state( + Provider::mocked_builder().mock_identity(identity_mock), + true, + None, + ) + .await; + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?type=local") + .extension(vsc) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + #[tokio::test] async fn test_list_unauth() { let state = get_mocked_state(Provider::mocked_builder(), false, None).await; From 81d625b922e23d963a51c42dff4abad87e6b74bf Mon Sep 17 00:00:00 2001 From: Mohammed Date: Mon, 22 Jun 2026 17:32:08 +0300 Subject: [PATCH 2/2] fix: Restrict user type filter to v4 only --- crates/api-types/src/v3/user.rs | 22 ----------- crates/api-types/src/v3/user_conv.rs | 14 +------ crates/api-types/src/v4.rs | 2 + crates/api-types/src/v4/user.rs | 49 +++++++++++++++++++++++- crates/api-types/src/v4/user_conv.rs | 40 +++++++++++++++++++ crates/keystone/src/api/v3/user/list.rs | 39 +------------------ crates/keystone/src/api/v4/user/types.rs | 5 ++- 7 files changed, 94 insertions(+), 77 deletions(-) create mode 100644 crates/api-types/src/v4/user_conv.rs diff --git a/crates/api-types/src/v3/user.rs b/crates/api-types/src/v3/user.rs index 384358e18..b515628fb 100644 --- a/crates/api-types/src/v3/user.rs +++ b/crates/api-types/src/v3/user.rs @@ -309,28 +309,6 @@ pub struct UserListParameters { /// Filter users by the federated unique ID. #[cfg_attr(feature = "validate", validate(length(max = 64)))] pub unique_id: Option, - - /// Filter users by type (`local`, `federated`, `nonlocal`, `all`). - #[serde(rename = "type")] - pub user_type: Option, -} - -/// User type filter for listing users. -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -#[serde(rename_all = "lowercase")] -pub enum UserType { - /// All users (default behavior). - All, - /// Federated users only. - Federated, - /// Local users only. - Local, - /// Non-local users only. - NonLocal, - /// Service account users only. - #[serde(rename = "service_account")] - ServiceAccount, } #[cfg(test)] diff --git a/crates/api-types/src/v3/user_conv.rs b/crates/api-types/src/v3/user_conv.rs index 5e0db3456..0bb78e0a9 100644 --- a/crates/api-types/src/v3/user_conv.rs +++ b/crates/api-types/src/v3/user_conv.rs @@ -111,25 +111,13 @@ impl From for api_types::FederationProtocol } } -impl From for provider_types::UserType { - fn from(value: api_types::UserType) -> Self { - match value { - api_types::UserType::All => Self::All, - api_types::UserType::Federated => Self::Federated, - api_types::UserType::Local => Self::Local, - api_types::UserType::NonLocal => Self::NonLocal, - api_types::UserType::ServiceAccount => Self::ServiceAccount, - } - } -} - impl From for provider_types::UserListParameters { fn from(value: api_types::UserListParameters) -> Self { Self { domain_id: value.domain_id, name: value.name, unique_id: value.unique_id, - user_type: value.user_type.map(Into::into), + ..Default::default() } } } diff --git a/crates/api-types/src/v4.rs b/crates/api-types/src/v4.rs index 60263d5f2..b22b1fd6d 100644 --- a/crates/api-types/src/v4.rs +++ b/crates/api-types/src/v4.rs @@ -19,3 +19,5 @@ pub mod user; #[cfg(feature = "conv")] mod token_restriction_conv; +#[cfg(feature = "conv")] +mod user_conv; diff --git a/crates/api-types/src/v4/user.rs b/crates/api-types/src/v4/user.rs index 973c20cef..a9fd031bd 100644 --- a/crates/api-types/src/v4/user.rs +++ b/crates/api-types/src/v4/user.rs @@ -13,7 +13,52 @@ // SPDX-License-Identifier: Apache-2.0 //! User resource types. +use serde::{Deserialize, Serialize}; + pub use crate::v3::user::{ - Federation, FederationProtocol, User, UserCreate, UserCreateRequest, UserList, - UserListParameters, UserOptions, UserResponse, UserUpdateRequest, + Federation, FederationProtocol, User, UserCreate, UserCreateRequest, UserList, UserOptions, + UserResponse, UserUpdateRequest, }; + +/// User list parameters. +/// +/// V4 extends the v3 listing with a `type` filter; this is intentionally a +/// v4-only parameter (Python Keystone does not support it on v3). +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))] +#[cfg_attr(feature = "validate", derive(validator::Validate))] +pub struct UserListParameters { + /// Filter users by Domain ID. + #[cfg_attr(feature = "validate", validate(length(max = 64)))] + pub domain_id: Option, + + /// Filter users by Name. + #[cfg_attr(feature = "validate", validate(length(max = 255)))] + pub name: Option, + + /// Filter users by the federated unique ID. + #[cfg_attr(feature = "validate", validate(length(max = 64)))] + pub unique_id: Option, + + /// Filter users by type (`local`, `federated`, `nonlocal`, `all`). + #[serde(rename = "type")] + pub user_type: Option, +} + +/// User type filter for listing users. +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "lowercase")] +pub enum UserType { + /// All users (default behavior). + All, + /// Federated users only. + Federated, + /// Local users only. + Local, + /// Non-local users only. + NonLocal, + /// Service account users only. + #[serde(rename = "service_account")] + ServiceAccount, +} diff --git a/crates/api-types/src/v4/user_conv.rs b/crates/api-types/src/v4/user_conv.rs new file mode 100644 index 000000000..10a5863e8 --- /dev/null +++ b/crates/api-types/src/v4/user_conv.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use openstack_keystone_core_types::identity as provider_types; + +use crate::v4::user as api_types; + +impl From for provider_types::UserType { + fn from(value: api_types::UserType) -> Self { + match value { + api_types::UserType::All => Self::All, + api_types::UserType::Federated => Self::Federated, + api_types::UserType::Local => Self::Local, + api_types::UserType::NonLocal => Self::NonLocal, + api_types::UserType::ServiceAccount => Self::ServiceAccount, + } + } +} + +impl From for provider_types::UserListParameters { + fn from(value: api_types::UserListParameters) -> Self { + Self { + domain_id: value.domain_id, + name: value.name, + unique_id: value.unique_id, + user_type: value.user_type.map(Into::into), + } + } +} diff --git a/crates/keystone/src/api/v3/user/list.rs b/crates/keystone/src/api/v3/user/list.rs index ebc45b172..67f015913 100644 --- a/crates/keystone/src/api/v3/user/list.rs +++ b/crates/keystone/src/api/v3/user/list.rs @@ -75,9 +75,7 @@ mod tests { use tower::ServiceExt; // for `call`, `oneshot`, and `ready` use tower_http::trace::TraceLayer; - use openstack_keystone_core_types::identity::{ - UserListParameters, UserResponseBuilder, UserType, - }; + use openstack_keystone_core_types::identity::{UserListParameters, UserResponseBuilder}; use super::super::openapi_router; use crate::api::tests::{get_mocked_state, test_fixture_scoped}; @@ -189,41 +187,6 @@ mod tests { let _res: UserList = serde_json::from_slice(&body).unwrap(); } - #[tokio::test] - async fn test_list_qp_type() { - let mut identity_mock = MockIdentityProvider::default(); - identity_mock - .expect_list_users() - .withf(|_, qp: &UserListParameters| qp.user_type == Some(UserType::Local)) - .returning(|_, _| Ok(Vec::new())); - - let vsc = test_fixture_scoped(); - let state = get_mocked_state( - Provider::mocked_builder().mock_identity(identity_mock), - true, - None, - ) - .await; - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/?type=local") - .extension(vsc) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - } - #[tokio::test] async fn test_list_unauth() { let state = get_mocked_state(Provider::mocked_builder(), false, None).await; diff --git a/crates/keystone/src/api/v4/user/types.rs b/crates/keystone/src/api/v4/user/types.rs index 6ebd16271..e0d947e33 100644 --- a/crates/keystone/src/api/v4/user/types.rs +++ b/crates/keystone/src/api/v4/user/types.rs @@ -14,6 +14,7 @@ //! User resource types. pub use crate::api::v3::user::types::{ - Federation, FederationProtocol, User, UserCreate, UserCreateRequest, UserList, - UserListParameters, UserOptions, UserResponse, UserUpdateRequest, + Federation, FederationProtocol, User, UserCreate, UserCreateRequest, UserList, UserOptions, + UserResponse, UserUpdateRequest, }; +pub use openstack_keystone_api_types::v4::user::UserListParameters;