Bug: feat(users): Add entity type filter in list users and list roles API



Currently list users and list roles API is sending all the users and roles respectively for those who are same entity level and less.

We need to add filters in these APIs to send users and roles who are only at particular entity level.
diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs
index 81b38198cfc..eb09abd471c 100644
--- a/crates/api_models/src/events/user_role.rs
+++ b/crates/api_models/src/events/user_role.rs
@@ -2,12 +2,12 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};
 
 use crate::user_role::{
     role::{
-        CreateRoleRequest, GetRoleRequest, ListRolesAtEntityLevelRequest, ListRolesResponse,
-        RoleInfoResponseNew, RoleInfoWithGroupsResponse, RoleInfoWithPermissionsResponse,
-        UpdateRoleRequest,
+        CreateRoleRequest, GetRoleRequest, ListRolesAtEntityLevelRequest, ListRolesRequest,
+        ListRolesResponse, RoleInfoResponseNew, RoleInfoWithGroupsResponse,
+        RoleInfoWithPermissionsResponse, UpdateRoleRequest,
     },
     AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest,
-    MerchantSelectRequest, UpdateUserRoleRequest,
+    ListUsersInEntityRequest, MerchantSelectRequest, UpdateUserRoleRequest,
 };
 
 common_utils::impl_api_event_type!(
@@ -25,6 +25,8 @@ common_utils::impl_api_event_type!(
         ListRolesResponse,
         ListRolesAtEntityLevelRequest,
         RoleInfoResponseNew,
-        RoleInfoWithGroupsResponse
+        RoleInfoWithGroupsResponse,
+        ListUsersInEntityRequest,
+        ListRolesRequest
     )
 );
diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs
index 8e1a0483c07..f2743d6a311 100644
--- a/crates/api_models/src/user_role.rs
+++ b/crates/api_models/src/user_role.rs
@@ -159,3 +159,8 @@ pub struct Entity {
     pub entity_id: String,
     pub entity_type: common_enums::EntityType,
 }
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct ListUsersInEntityRequest {
+    pub entity_type: Option<common_enums::EntityType>,
+}
diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs
index 828dfeb20f8..be467421e65 100644
--- a/crates/api_models/src/user_role/role.rs
+++ b/crates/api_models/src/user_role/role.rs
@@ -35,6 +35,11 @@ pub struct RoleInfoWithGroupsResponse {
     pub role_scope: RoleScope,
 }
 
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct ListRolesRequest {
+    pub entity_type: Option<EntityType>,
+}
+
 #[derive(Debug, serde::Serialize)]
 pub struct RoleInfoResponseNew {
     pub role_id: String,
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index d42f71695a5..fd9c8c89031 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -673,6 +673,7 @@ pub async fn delete_user_role(
 pub async fn list_users_in_lineage(
     state: SessionState,
     user_from_token: auth::UserFromToken,
+    request: user_role_api::ListUsersInEntityRequest,
 ) -> UserResponse<Vec<user_role_api::ListUsersInEntityResponse>> {
     let requestor_role_info = roles::RoleInfo::from_role_id_in_merchant_scope(
         &state,
@@ -683,51 +684,55 @@ pub async fn list_users_in_lineage(
     .await
     .change_context(UserErrors::InternalServerError)?;
 
-    let user_roles_set: HashSet<_> = match requestor_role_info.get_entity_type() {
-        EntityType::Organization => state
-            .store
-            .list_user_roles_by_org_id(ListUserRolesByOrgIdPayload {
-                user_id: None,
-                org_id: &user_from_token.org_id,
-                merchant_id: None,
-                profile_id: None,
-                version: None,
-            })
-            .await
-            .change_context(UserErrors::InternalServerError)?
-            .into_iter()
-            .collect(),
-        EntityType::Merchant => state
-            .store
-            .list_user_roles_by_org_id(ListUserRolesByOrgIdPayload {
-                user_id: None,
-                org_id: &user_from_token.org_id,
-                merchant_id: Some(&user_from_token.merchant_id),
-                profile_id: None,
-                version: None,
-            })
-            .await
-            .change_context(UserErrors::InternalServerError)?
-            .into_iter()
-            .collect(),
+    let user_roles_set: HashSet<_> = match utils::user_role::get_min_entity(
+        requestor_role_info.get_entity_type(),
+        request.entity_type,
+    )? {
+        EntityType::Organization => {
+            utils::user_role::fetch_user_roles_by_payload(
+                &state,
+                ListUserRolesByOrgIdPayload {
+                    user_id: None,
+                    org_id: &user_from_token.org_id,
+                    merchant_id: None,
+                    profile_id: None,
+                    version: None,
+                },
+                request.entity_type,
+            )
+            .await?
+        }
+        EntityType::Merchant => {
+            utils::user_role::fetch_user_roles_by_payload(
+                &state,
+                ListUserRolesByOrgIdPayload {
+                    user_id: None,
+                    org_id: &user_from_token.org_id,
+                    merchant_id: Some(&user_from_token.merchant_id),
+                    profile_id: None,
+                    version: None,
+                },
+                request.entity_type,
+            )
+            .await?
+        }
         EntityType::Profile => {
             let Some(profile_id) = user_from_token.profile_id.as_ref() else {
                 return Err(UserErrors::JwtProfileIdMissing.into());
             };
 
-            state
-                .store
-                .list_user_roles_by_org_id(ListUserRolesByOrgIdPayload {
+            utils::user_role::fetch_user_roles_by_payload(
+                &state,
+                ListUserRolesByOrgIdPayload {
                     user_id: None,
                     org_id: &user_from_token.org_id,
                     merchant_id: Some(&user_from_token.merchant_id),
                     profile_id: Some(profile_id),
                     version: None,
-                })
-                .await
-                .change_context(UserErrors::InternalServerError)?
-                .into_iter()
-                .collect()
+                },
+                request.entity_type,
+            )
+            .await?
         }
         EntityType::Internal => HashSet::new(),
     };
diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs
index 571ae7f995d..ffc18214c20 100644
--- a/crates/router/src/core/user_role/role.rs
+++ b/crates/router/src/core/user_role/role.rs
@@ -217,6 +217,7 @@ pub async fn update_role(
 pub async fn list_roles_with_info(
     state: SessionState,
     user_from_token: UserFromToken,
+    request: role_api::ListRolesRequest,
 ) -> UserResponse<Vec<role_api::RoleInfoResponseNew>> {
     let user_role_info = user_from_token
         .get_role_info_from_db(&state)
@@ -229,49 +230,57 @@ pub async fn list_roles_with_info(
         .collect::<Vec<_>>();
 
     let user_role_entity = user_role_info.get_entity_type();
-    let custom_roles = match user_role_entity {
-        EntityType::Organization => state
-            .store
-            .list_roles_for_org_by_parameters(&user_from_token.org_id, None, None, None)
-            .await
-            .change_context(UserErrors::InternalServerError)
-            .attach_printable("Failed to get roles")?,
-        EntityType::Merchant => state
-            .store
-            .list_roles_for_org_by_parameters(
-                &user_from_token.org_id,
-                Some(&user_from_token.merchant_id),
-                None,
-                None,
-            )
-            .await
-            .change_context(UserErrors::InternalServerError)
-            .attach_printable("Failed to get roles")?,
-        // TODO: Populate this from Db function when support for profile id and profile level custom roles is added
-        EntityType::Profile => Vec::new(),
-        EntityType::Internal => {
-            return Err(UserErrors::InvalidRoleOperationWithMessage(
-                "Internal roles are not allowed for this operation".to_string(),
-            )
-            .into());
-        }
-    };
+    let custom_roles =
+        match utils::user_role::get_min_entity(user_role_entity, request.entity_type)? {
+            EntityType::Organization => state
+                .store
+                .list_roles_for_org_by_parameters(
+                    &user_from_token.org_id,
+                    None,
+                    request.entity_type,
+                    None,
+                )
+                .await
+                .change_context(UserErrors::InternalServerError)
+                .attach_printable("Failed to get roles")?,
+            EntityType::Merchant => state
+                .store
+                .list_roles_for_org_by_parameters(
+                    &user_from_token.org_id,
+                    Some(&user_from_token.merchant_id),
+                    request.entity_type,
+                    None,
+                )
+                .await
+                .change_context(UserErrors::InternalServerError)
+                .attach_printable("Failed to get roles")?,
+            // TODO: Populate this from Db function when support for profile id and profile level custom roles is added
+            EntityType::Profile => Vec::new(),
+            EntityType::Internal => {
+                return Err(UserErrors::InvalidRoleOperationWithMessage(
+                    "Internal roles are not allowed for this operation".to_string(),
+                )
+                .into());
+            }
+        };
 
     role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from));
+
     let list_role_info_response = role_info_vec
         .into_iter()
         .filter_map(|role_info| {
-            if user_role_entity >= role_info.get_entity_type() {
-                Some(role_api::RoleInfoResponseNew {
-                    role_id: role_info.get_role_id().to_string(),
-                    role_name: role_info.get_role_name().to_string(),
-                    groups: role_info.get_permission_groups().to_vec(),
-                    entity_type: role_info.get_entity_type(),
-                    scope: role_info.get_scope(),
-                })
-            } else {
-                None
-            }
+            let is_lower_entity = user_role_entity >= role_info.get_entity_type();
+            let request_filter = request.entity_type.map_or(true, |entity_type| {
+                entity_type == role_info.get_entity_type()
+            });
+
+            (is_lower_entity && request_filter).then_some(role_api::RoleInfoResponseNew {
+                role_id: role_info.get_role_id().to_string(),
+                role_name: role_info.get_role_name().to_string(),
+                groups: role_info.get_permission_groups().to_vec(),
+                entity_type: role_info.get_entity_type(),
+                scope: role_info.get_scope(),
+            })
         })
         .collect::<Vec<_>>();
 
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index b1260b5d7ad..922e7612b14 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -291,16 +291,20 @@ pub async fn get_role_information(
     .await
 }
 
-pub async fn list_users_in_lineage(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
+pub async fn list_users_in_lineage(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    query: web::Query<user_role_api::ListUsersInEntityRequest>,
+) -> HttpResponse {
     let flow = Flow::ListUsersInLineage;
 
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
-        (),
-        |state, user_from_token, _, _| {
-            user_role_core::list_users_in_lineage(state, user_from_token)
+        query.into_inner(),
+        |state, user_from_token, request, _| {
+            user_role_core::list_users_in_lineage(state, user_from_token, request)
         },
         &auth::DashboardNoPermissionAuth,
         api_locking::LockAction::NotApplicable,
@@ -308,15 +312,21 @@ pub async fn list_users_in_lineage(state: web::Data<AppState>, req: HttpRequest)
     .await
 }
 
-pub async fn list_roles_with_info(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
+pub async fn list_roles_with_info(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    query: web::Query<role_api::ListRolesRequest>,
+) -> HttpResponse {
     let flow = Flow::ListRolesV2;
 
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
-        (),
-        |state, user_from_token, _, _| role_core::list_roles_with_info(state, user_from_token),
+        query.into_inner(),
+        |state, user_from_token, request, _| {
+            role_core::list_roles_with_info(state, user_from_token, request)
+        },
         &auth::JWTAuth {
             permission: Permission::UsersRead,
             minimum_entity_level: EntityType::Profile,
diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs
index b4008bd0008..9141b91bdd6 100644
--- a/crates/router/src/utils/user_role.rs
+++ b/crates/router/src/utils/user_role.rs
@@ -14,7 +14,7 @@ use storage_impl::errors::StorageError;
 use crate::{
     consts,
     core::errors::{UserErrors, UserResult},
-    db::user_role::ListUserRolesByUserIdPayload,
+    db::user_role::{ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload},
     routes::SessionState,
     services::authorization::{self as authz, permissions::Permission, roles},
     types::domain,
@@ -398,3 +398,61 @@ pub async fn get_single_merchant_id_and_profile_id(
 
     Ok((merchant_id, profile_id))
 }
+
+pub async fn fetch_user_roles_by_payload(
+    state: &SessionState,
+    payload: ListUserRolesByOrgIdPayload<'_>,
+    request_entity_type: Option<EntityType>,
+) -> UserResult<HashSet<UserRole>> {
+    Ok(state
+        .store
+        .list_user_roles_by_org_id(payload)
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into_iter()
+        .filter_map(|user_role| {
+            let (_entity_id, entity_type) = user_role.get_entity_id_and_type()?;
+            request_entity_type
+                .map_or(true, |req_entity_type| entity_type == req_entity_type)
+                .then_some(user_role)
+        })
+        .collect::<HashSet<_>>())
+}
+
+pub fn get_min_entity(
+    user_entity: EntityType,
+    filter_entity: Option<EntityType>,
+) -> UserResult<EntityType> {
+    match (user_entity, filter_entity) {
+        (EntityType::Organization, None)
+        | (EntityType::Organization, Some(EntityType::Organization)) => {
+            Ok(EntityType::Organization)
+        }
+
+        (EntityType::Merchant, None)
+        | (EntityType::Organization, Some(EntityType::Merchant))
+        | (EntityType::Merchant, Some(EntityType::Merchant)) => Ok(EntityType::Merchant),
+
+        (EntityType::Profile, None)
+        | (EntityType::Organization, Some(EntityType::Profile))
+        | (EntityType::Merchant, Some(EntityType::Profile))
+        | (EntityType::Profile, Some(EntityType::Profile)) => Ok(EntityType::Profile),
+
+        (EntityType::Internal, _) => Ok(EntityType::Internal),
+
+        (EntityType::Organization, Some(EntityType::Internal))
+        | (EntityType::Merchant, Some(EntityType::Internal))
+        | (EntityType::Profile, Some(EntityType::Internal)) => {
+            Err(UserErrors::InvalidRoleOperation.into())
+        }
+
+        (EntityType::Merchant, Some(EntityType::Organization))
+        | (EntityType::Profile, Some(EntityType::Organization))
+        | (EntityType::Profile, Some(EntityType::Merchant)) => {
+            Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!(
+                "{} level user requesting data for {:?} level",
+                user_entity, filter_entity
+            ))
+        }
+    }
+}

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
- Adds entity_type filter in list users and list roles API.

### Additional Changes

- [x] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
Closes #5996.

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->

1. List Users
```
curl 'http://localhost:8080/user/user/v2/list' \
  -H 'authorization: Bearer JWT' \
```
This should respond with all the users who are at the same level and lower.

```
curl 'http://localhost:8080/user/user/v2/list?entity_type=organization' \
  -H 'authorization: Bearer JWT' \
```
This can only be hit by org level users and only responds with org level users.

```
curl 'http://localhost:8080/user/user/v2/list?entity_type=merchant' \
  -H 'authorization: Bearer JWT' \
```
This can only be hit by merchant and org level users and only responds with merchant level users.

```
curl 'http://localhost:8080/user/user/v2/list?entity_type=profile' \
  -H 'authorization: Bearer JWT' \
```
This can only be hit by profile, merchant and org level users and only responds with profile level users.


Response for all the above curls should follow the same structure
```json
[
    {
        "email": "email",
        "roles": [
            {
                "role_id": "merchant_view_only",
                "role_name": "merchant_view_only"
            }
        ]
    },
    {
        "email": "email",
        "roles": [
            {
                "role_id": "merchant_view_only",
                "role_name": "merchant_view_only"
            }
        ]
    },
    {
        "email": "email",
        "roles": [
            {
                "role_id": "merchant_view_only",
                "role_name": "merchant_view_only"
            }
        ]
    },
    {
        "email": "email",
        "roles": [
            {
                "role_id": "merchant_view_only",
                "role_name": "merchant_view_only"
            }
        ]
    }
]
```
2. List Roles
```
curl 'http://localhost:8080/user/role/v2/list' \                                                                                                                            
  -H 'authorization: Bearer JWT''
```

```
curl 'http://localhost:8080/user/role/v2/list?entity_type=merchant' \                                                                                                                            
  -H 'authorization: Bearer JWT''
```
This can only be hit by org and merchant level users and only responds with merchant level roles.


```
curl 'http://localhost:8080/user/role/v2/list?entity_type=profile' \                                                                                                                            
  -H 'authorization: Bearer JWT''
```
This can only be hit by org, merchant and profile level users and only responds with profile level roles.

```
curl 'http://localhost:8080/user/role/v2/list?entity_type=organization' \                                                                                                                            
  -H 'authorization: Bearer JWT''
```
This can only be hit by org level users and only responds with org level roles.


Response should look like this:
```json
[
  {
    "role_id": "merchant_iam_admin",
    "role_name": "merchant_iam",
    "entity_type": "merchant",
    "groups": [
      "operations_view",
      "analytics_view",
      "users_view",
      "users_manage",
      "merchant_details_view"
    ],
    "scope": "organization"
  },
  {
    "role_id": "merchant_developer",
    "role_name": "merchant_developer",
    "entity_type": "merchant",
    "groups": [
      "operations_view",
      "connectors_view",
      "analytics_view",
      "users_view",
      "merchant_details_view",
      "merchant_details_manage"
    ],
    "scope": "organization"
  },
  {
    "role_id": "merchant_customer_support",
    "role_name": "customer_support",
    "entity_type": "merchant",
    "groups": [
      "operations_view",
      "analytics_view",
      "users_view",
      "merchant_details_view"
    ],
    "scope": "organization"
  },
  {
    "role_id": "profile_customer_support",
    "role_name": "profile_customer_support",
    "entity_type": "profile",
    "groups": [
      "operations_view",
      "analytics_view",
      "users_view",
      "merchant_details_view"
    ],
    "scope": "organization"
  },
  {
    "role_id": "merchant_admin",
    "role_name": "merchant_admin",
    "entity_type": "merchant",
    "groups": [
      "operations_view",
      "operations_manage",
      "connectors_view",
      "connectors_manage",
      "workflows_view",
      "workflows_manage",
      "analytics_view",
      "users_view",
      "users_manage",
      "merchant_details_view",
      "merchant_details_manage",
      "recon_ops"
    ],
    "scope": "organization"
  },
  {
    "role_id": "merchant_operator",
    "role_name": "merchant_operator",
    "entity_type": "merchant",
    "groups": [
      "operations_view",
      "operations_manage",
      "connectors_view",
      "workflows_view",
      "analytics_view",
      "users_view",
      "merchant_details_view"
    ],
    "scope": "organization"
  },
  {
    "role_id": "profile_operator",
    "role_name": "profile_operator",
    "entity_type": "profile",
    "groups": [
      "operations_view",
      "operations_manage",
      "connectors_view",
      "workflows_view",
      "analytics_view",
      "users_view",
      "merchant_details_view"
    ],
    "scope": "organization"
  },
  {
    "role_id": "org_admin",
    "role_name": "organization_admin",
    "entity_type": "organization",
    "groups": [
      "operations_view",
      "operations_manage",
      "connectors_view",
      "connectors_manage",
      "workflows_view",
      "workflows_manage",
      "analytics_view",
      "users_view",
      "users_manage",
      "merchant_details_view",
      "merchant_details_manage",
      "organization_manage",
      "recon_ops"
    ],
    "scope": "organization"
  }
]
```

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
