Issue:
Bug: [FEATURE] add parent info based API for fetching permissions for user role



add parent info based API for fetching permissions for user role so backend permissions and frontend permissions can remain consistent.

Pull request description:
## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [x] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
Adds a new API endpoint GET /user/role/v3 that returns user role permissions using parent group information with resource details.


### Additional Changes

- [ ] 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

This PR adds a new API endpoint that provides structured parent group info with resources and scopes, enabling the frontend to implement proper resource + scope based permission checks.


## How did you test it?

```
curl --location 'http://localhost:9000/api/user/role/v3' \
--header 'Cookie: *******

```

Response:
```
[
    {
        "name": "ReconReports",
        "resources": [
            "recon_token",
            "recon_and_settlement_analytics",
            "recon_reports",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Analytics",
        "resources": [
            "analytics",
            "report",
            "account"
        ],
        "scopes": [
            "read"
        ]
    },
    {
        "name": "Theme",
        "resources": [
            "theme"
        ],
        "scopes": [
            "read",
            "write"
        ]
    },
    {
        "name": "Operations",
        "resources": [
            "payment",
            "refund",
            "mandate",
            "dispute",
            "customer",
            "payout",
            "report",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Connectors",
        "resources": [
            "connector",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Users",
        "resources": [
            "user",
            "account"
        ],
        "scopes": [
            "read",
            "write"
        ]
    },
    {
        "name": "Workflows",
        "resources": [
            "routing",
            "three_ds_decision_manager",
            "surcharge_decision_manager",
            "account",
            "revenue_recovery"
        ],
        "scopes": [
            "read",
            "write"
        ]
    },
    {
        "name": "ReconOps",
        "resources": [
            "recon_token",
            "recon_files",
            "recon_upload",
            "run_recon",
            "recon_config",
            "recon_and_settlement_analytics",
            "recon_reports",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Account",
        "resources": [
            "account",
            "api_key",
            "webhook_event"
        ],
        "scopes": [
            "write",
            "read"
        ]
    }
]

```
## Checklist
<!-- Put an `x` in the boxes that apply -->

- [ ] I formatted the code `cargo +nightly fmt --all`
- [ ] I addressed lints thrown by `cargo clippy`
- [ ] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible


Testing:
```
curl --location 'http://localhost:9000/api/user/role/v3' \
--header 'Cookie: *******

```

Response:
```
[
    {
        "name": "ReconReports",
        "resources": [
            "recon_token",
            "recon_and_settlement_analytics",
            "recon_reports",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Analytics",
        "resources": [
            "analytics",
            "report",
            "account"
        ],
        "scopes": [
            "read"
        ]
    },
    {
        "name": "Theme",
        "resources": [
            "theme"
        ],
        "scopes": [
            "read",
            "write"
        ]
    },
    {
        "name": "Operations",
        "resources": [
            "payment",
            "refund",
            "mandate",
            "dispute",
            "customer",
            "payout",
            "report",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Connectors",
        "resources": [
            "connector",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Users",
        "resources": [
            "user",
            "account"
        ],
        "scopes": [
            "read",
            "write"
        ]
    },
    {
        "name": "Workflows",
        "resources": [
            "routing",
            "three_ds_decision_manager",
            "surcharge_decision_manager",
            "account",
            "revenue_recovery"
        ],
        "scopes": [
            "read",
            "write"
        ]
    },
    {
        "name": "ReconOps",
        "resources": [
            "recon_token",
            "recon_files",
            "recon_upload",
            "run_recon",
            "recon_config",
            "recon_and_settlement_analytics",
            "recon_reports",
            "account"
        ],
        "scopes": [
            "write",
            "read"
        ]
    },
    {
        "name": "Account",
        "resources": [
            "account",
            "api_key",
            "webhook_event"
        ],
        "scopes": [
            "write",
            "read"
        ]
    }
]

```

Patch:
diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs
index 0ab853273e7..b0cc9aa8554 100644
--- a/crates/api_models/src/user_role/role.rs
+++ b/crates/api_models/src/user_role/role.rs
@@ -36,13 +36,13 @@ pub struct RoleInfoWithGroupsResponse {
 #[derive(Debug, serde::Serialize)]
 pub struct RoleInfoWithParents {
     pub role_id: String,
-    pub parent_groups: Vec<ParentGroupInfo>,
+    pub parent_groups: Vec<ParentGroupDescription>,
     pub role_name: String,
     pub role_scope: RoleScope,
 }
 
 #[derive(Debug, serde::Serialize)]
-pub struct ParentGroupInfo {
+pub struct ParentGroupDescription {
     pub name: ParentGroup,
     pub description: String,
     pub scopes: Vec<PermissionScope>,
@@ -74,7 +74,7 @@ pub struct RoleInfoResponseWithParentsGroup {
     pub role_id: String,
     pub role_name: String,
     pub entity_type: EntityType,
-    pub parent_groups: Vec<ParentGroupInfo>,
+    pub parent_groups: Vec<ParentGroupDescription>,
     pub role_scope: RoleScope,
 }
 
@@ -117,3 +117,10 @@ pub enum ListRolesResponse {
     WithGroups(Vec<RoleInfoResponseNew>),
     WithParentGroups(Vec<RoleInfoResponseWithParentsGroup>),
 }
+
+#[derive(Debug, serde::Serialize)]
+pub struct ParentGroupInfo {
+    pub name: ParentGroup,
+    pub resources: Vec<Resource>,
+    pub scopes: Vec<PermissionScope>,
+}
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index 1b4870f05ea..d6a55ffdbf1 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -90,7 +90,7 @@ pub async fn get_parent_group_info(
     state: SessionState,
     user_from_token: auth::UserFromToken,
     request: role_api::GetParentGroupsInfoQueryParams,
-) -> UserResponse<Vec<role_api::ParentGroupInfo>> {
+) -> UserResponse<Vec<role_api::ParentGroupDescription>> {
     let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id(
         &state,
         &user_from_token.role_id,
@@ -119,17 +119,21 @@ pub async fn get_parent_group_info(
         ParentGroup::get_descriptions_for_groups(entity_type, PermissionGroup::iter().collect())
             .unwrap_or_default()
             .into_iter()
-            .map(|(parent_group, description)| role_api::ParentGroupInfo {
-                name: parent_group.clone(),
-                description,
-                scopes: PermissionGroup::iter()
-                    .filter_map(|group| (group.parent() == parent_group).then_some(group.scope()))
-                    // TODO: Remove this hashset conversion when merchant access
-                    // and organization access groups are removed
-                    .collect::<HashSet<_>>()
-                    .into_iter()
-                    .collect(),
-            })
+            .map(
+                |(parent_group, description)| role_api::ParentGroupDescription {
+                    name: parent_group.clone(),
+                    description,
+                    scopes: PermissionGroup::iter()
+                        .filter_map(|group| {
+                            (group.parent() == parent_group).then_some(group.scope())
+                        })
+                        // TODO: Remove this hashset conversion when merchant access
+                        // and organization access groups are removed
+                        .collect::<HashSet<_>>()
+                        .into_iter()
+                        .collect(),
+                },
+            )
             .collect::<Vec<_>>();
 
     Ok(ApplicationResponse::Json(parent_groups))
diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs
index cbd49a14391..e0f266c976a 100644
--- a/crates/router/src/core/user_role/role.rs
+++ b/crates/router/src/core/user_role/role.rs
@@ -58,6 +58,25 @@ pub async fn get_groups_and_resources_for_role_from_token(
     }))
 }
 
+pub async fn get_parent_groups_info_for_role_from_token(
+    state: SessionState,
+    user_from_token: UserFromToken,
+) -> UserResponse<Vec<role_api::ParentGroupInfo>> {
+    let role_info = user_from_token.get_role_info_from_db(&state).await?;
+
+    let groups = role_info
+        .get_permission_groups()
+        .into_iter()
+        .collect::<Vec<_>>();
+
+    let parent_groups = utils::user_role::permission_groups_to_parent_group_info(
+        &groups,
+        role_info.get_entity_type(),
+    );
+
+    Ok(ApplicationResponse::Json(parent_groups))
+}
+
 pub async fn create_role(
     state: SessionState,
     user_from_token: UserFromToken,
@@ -248,16 +267,31 @@ pub async fn create_role_v2(
         .await
         .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?;
 
-    let response_parent_groups =
+    let parent_group_details =
         utils::user_role::permission_groups_to_parent_group_info(&role.groups, role.entity_type);
 
+    let parent_group_descriptions: Vec<role_api::ParentGroupDescription> = parent_group_details
+        .into_iter()
+        .filter_map(|group_details| {
+            let description = utils::user_role::resources_to_description(
+                group_details.resources,
+                role.entity_type,
+            )?;
+            Some(role_api::ParentGroupDescription {
+                name: group_details.name,
+                description,
+                scopes: group_details.scopes,
+            })
+        })
+        .collect();
+
     Ok(ApplicationResponse::Json(
         role_api::RoleInfoResponseWithParentsGroup {
             role_id: role.role_id,
             role_name: role.role_name,
             role_scope: role.scope,
             entity_type: role.entity_type,
-            parent_groups: response_parent_groups,
+            parent_groups: parent_group_descriptions,
         },
     ))
 }
@@ -325,19 +359,21 @@ pub async fn get_parent_info_for_role(
         role.role_id
     ))?
     .into_iter()
-    .map(|(parent_group, description)| role_api::ParentGroupInfo {
-        name: parent_group.clone(),
-        description,
-        scopes: role_info
-            .get_permission_groups()
-            .iter()
-            .filter_map(|group| (group.parent() == parent_group).then_some(group.scope()))
-            // TODO: Remove this hashset conversion when merchant access
-            // and organization access groups are removed
-            .collect::<HashSet<_>>()
-            .into_iter()
-            .collect(),
-    })
+    .map(
+        |(parent_group, description)| role_api::ParentGroupDescription {
+            name: parent_group.clone(),
+            description,
+            scopes: role_info
+                .get_permission_groups()
+                .iter()
+                .filter_map(|group| (group.parent() == parent_group).then_some(group.scope()))
+                // TODO: Remove this hashset conversion when merchant access
+                // and organization access groups are removed
+                .collect::<HashSet<_>>()
+                .into_iter()
+                .collect(),
+        },
+    )
     .collect();
 
     Ok(ApplicationResponse::Json(role_api::RoleInfoWithParents {
@@ -513,16 +549,33 @@ pub async fn list_roles_with_info(
 
                 (is_lower_entity && request_filter).then_some({
                     let permission_groups = role_info.get_permission_groups();
-                    let parent_groups = utils::user_role::permission_groups_to_parent_group_info(
-                        &permission_groups,
-                        role_info.get_entity_type(),
-                    );
+                    let parent_group_details =
+                        utils::user_role::permission_groups_to_parent_group_info(
+                            &permission_groups,
+                            role_info.get_entity_type(),
+                        );
+
+                    let parent_group_descriptions: Vec<role_api::ParentGroupDescription> =
+                        parent_group_details
+                            .into_iter()
+                            .filter_map(|group_details| {
+                                let description = utils::user_role::resources_to_description(
+                                    group_details.resources,
+                                    role_info.get_entity_type(),
+                                )?;
+                                Some(role_api::ParentGroupDescription {
+                                    name: group_details.name,
+                                    description,
+                                    scopes: group_details.scopes,
+                                })
+                            })
+                            .collect();
 
                     role_api::RoleInfoResponseWithParentsGroup {
                         role_id: role_info.get_role_id().to_string(),
                         role_name: role_info.get_role_name().to_string(),
                         entity_type: role_info.get_entity_type(),
-                        parent_groups,
+                        parent_groups: parent_group_descriptions,
                         role_scope: role_info.get_scope(),
                     }
                 })
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index ef3f8081b9a..f36a0bf4ba3 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -2730,51 +2730,54 @@ impl User {
         }
 
         // Role information
-        route = route.service(
-            web::scope("/role")
-                .service(
-                    web::resource("")
-                        .route(web::get().to(user_role::get_role_from_token))
-                        // TODO: To be deprecated
-                        .route(web::post().to(user_role::create_role)),
-                )
-                .service(
-                    web::resource("/v2")
-                        .route(web::post().to(user_role::create_role_v2))
-                        .route(
-                            web::get().to(user_role::get_groups_and_resources_for_role_from_token),
-                        ),
-                )
-                // TODO: To be deprecated
-                .service(
-                    web::resource("/v2/list").route(web::get().to(user_role::list_roles_with_info)),
-                )
-                .service(
-                    web::scope("/list")
-                        .service(
-                            web::resource("").route(web::get().to(user_role::list_roles_with_info)),
-                        )
-                        .service(
-                            web::resource("/invite").route(
-                                web::get().to(user_role::list_invitable_roles_at_entity_level),
+        route =
+            route.service(
+                web::scope("/role")
+                    .service(
+                        web::resource("")
+                            .route(web::get().to(user_role::get_role_from_token))
+                            // TODO: To be deprecated
+                            .route(web::post().to(user_role::create_role)),
+                    )
+                    .service(
+                        web::resource("/v2")
+                            .route(web::post().to(user_role::create_role_v2))
+                            .route(
+                                web::get()
+                                    .to(user_role::get_groups_and_resources_for_role_from_token),
                             ),
-                        )
-                        .service(
-                            web::resource("/update").route(
+                    )
+                    .service(web::resource("/v3").route(
+                        web::get().to(user_role::get_parent_groups_info_for_role_from_token),
+                    ))
+                    // TODO: To be deprecated
+                    .service(
+                        web::resource("/v2/list")
+                            .route(web::get().to(user_role::list_roles_with_info)),
+                    )
+                    .service(
+                        web::scope("/list")
+                            .service(
+                                web::resource("")
+                                    .route(web::get().to(user_role::list_roles_with_info)),
+                            )
+                            .service(web::resource("/invite").route(
+                                web::get().to(user_role::list_invitable_roles_at_entity_level),
+                            ))
+                            .service(web::resource("/update").route(
                                 web::get().to(user_role::list_updatable_roles_at_entity_level),
-                            ),
-                        ),
-                )
-                .service(
-                    web::resource("/{role_id}")
-                        .route(web::get().to(user_role::get_role))
-                        .route(web::put().to(user_role::update_role)),
-                )
-                .service(
-                    web::resource("/{role_id}/v2")
-                        .route(web::get().to(user_role::get_parent_info_for_role)),
-                ),
-        );
+                            )),
+                    )
+                    .service(
+                        web::resource("/{role_id}")
+                            .route(web::get().to(user_role::get_role))
+                            .route(web::put().to(user_role::update_role)),
+                    )
+                    .service(
+                        web::resource("/{role_id}/v2")
+                            .route(web::get().to(user_role::get_parent_info_for_role)),
+                    ),
+            );
 
         #[cfg(feature = "dummy_connector")]
         {
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 0c42909b457..04745ea15b0 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -324,6 +324,7 @@ impl From<Flow> for ApiIdentifier {
             | Flow::GetRoleV2
             | Flow::GetRoleFromToken
             | Flow::GetRoleFromTokenV2
+            | Flow::GetParentGroupsInfoForRoleFromToken
             | Flow::UpdateUserRole
             | Flow::GetAuthorizationInfo
             | Flow::GetRolesInfo
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index 91cc92cb3e2..6247761d617 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -74,6 +74,27 @@ pub async fn get_groups_and_resources_for_role_from_token(
     ))
     .await
 }
+
+pub async fn get_parent_groups_info_for_role_from_token(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+) -> HttpResponse {
+    let flow = Flow::GetParentGroupsInfoForRoleFromToken;
+
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        (),
+        |state, user, _, _| async move {
+            role_core::get_parent_groups_info_for_role_from_token(state, user).await
+        },
+        &auth::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 // TODO: To be deprecated
 pub async fn create_role(
     state: web::Data<AppState>,
diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs
index 774368b18d6..6a2ce1c8397 100644
--- a/crates/router/src/services/authorization/permission_groups.rs
+++ b/crates/router/src/services/authorization/permission_groups.rs
@@ -3,7 +3,7 @@ use std::{collections::HashMap, ops::Not};
 use common_enums::{EntityType, ParentGroup, PermissionGroup, PermissionScope, Resource};
 use strum::IntoEnumIterator;
 
-use super::permissions::{self, ResourceExt};
+use super::permissions;
 
 pub trait PermissionGroupExt {
     fn scope(&self) -> PermissionScope;
@@ -147,15 +147,8 @@ impl ParentGroupExt for ParentGroup {
                 if !groups.iter().any(|group| group.parent() == parent) {
                     return None;
                 }
-                let filtered_resources: Vec<_> = parent
-                    .resources()
-                    .into_iter()
-                    .filter(|res| res.entities().iter().any(|entity| entity <= &entity_type))
-                    .collect();
-
-                if filtered_resources.is_empty() {
-                    return None;
-                }
+                let filtered_resources =
+                    permissions::filter_resources_by_entity_type(parent.resources(), entity_type)?;
 
                 let description = filtered_resources
                     .iter()
diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs
index 3860d343b68..b890df41468 100644
--- a/crates/router/src/services/authorization/permissions.rs
+++ b/crates/router/src/services/authorization/permissions.rs
@@ -152,3 +152,15 @@ pub fn get_scope_name(scope: PermissionScope) -> &'static str {
         PermissionScope::Write => "View and Manage",
     }
 }
+
+pub fn filter_resources_by_entity_type(
+    resources: Vec<Resource>,
+    entity_type: EntityType,
+) -> Option<Vec<Resource>> {
+    let filtered: Vec<Resource> = resources
+        .into_iter()
+        .filter(|res| res.entities().iter().any(|entity| entity <= &entity_type))
+        .collect();
+
+    (!filtered.is_empty()).then_some(filtered)
+}
diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs
index 67ed2127eab..bcaea767a22 100644
--- a/crates/router/src/utils/user_role.rs
+++ b/crates/router/src/utils/user_role.rs
@@ -27,7 +27,7 @@ use crate::{
     services::authorization::{
         self as authz,
         permission_groups::{ParentGroupExt, PermissionGroupExt},
-        roles,
+        permissions, roles,
     },
     types::domain,
 };
@@ -570,15 +570,33 @@ pub fn permission_groups_to_parent_group_info(
                 .into_iter()
                 .collect();
 
-            let description =
-                ParentGroup::get_descriptions_for_groups(entity_type, permission_groups.to_vec())
-                    .and_then(|descriptions| descriptions.get(&name).cloned())?;
+            let filtered_resources =
+                permissions::filter_resources_by_entity_type(name.resources(), entity_type)?;
 
             Some(role_api::ParentGroupInfo {
                 name,
-                description,
+                resources: filtered_resources,
                 scopes: unique_scopes,
             })
         })
         .collect()
 }
+
+pub fn resources_to_description(
+    resources: Vec<common_enums::Resource>,
+    entity_type: EntityType,
+) -> Option<String> {
+    if resources.is_empty() {
+        return None;
+    }
+
+    let filtered_resources = permissions::filter_resources_by_entity_type(resources, entity_type)?;
+
+    let description = filtered_resources
+        .iter()
+        .map(|res| permissions::get_resource_name(*res, entity_type))
+        .collect::<Option<Vec<_>>>()?
+        .join(", ");
+
+    Some(description)
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index ae5f34fd78f..33db02fbf0c 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -417,6 +417,8 @@ pub enum Flow {
     GetRoleFromToken,
     /// Get resources and groups for role from token
     GetRoleFromTokenV2,
+    /// Get parent groups info for role from token
+    GetParentGroupsInfoForRoleFromToken,
     /// Update user role
     UpdateUserRole,
     /// Create merchant account for user in a org