From eb9ca869677d7ff6e0f4b829b2961af88bbf61f1 Mon Sep 17 00:00:00 2001 From: vishnu r kumar Date: Mon, 15 Jun 2026 13:53:27 +0530 Subject: [PATCH 1/5] chore: update & optimize admin user flows --- .../controllers/superset_controller.py | 27 +---- .../llm_inference_config_controller.py | 28 +---- .../controllers/authenticator_controller.py | 16 +-- .../controllers/datasource_controller.py | 30 ++--- .../services/datasource_services.py | 19 ---- .../user_management_module/constants/auth.py | 2 + .../controllers/access_controller.py | 26 +---- .../controllers/user_controller.py | 53 ++++----- .../services/user_service.py | 106 +++++++++++------- .../utils/user_utils.py | 11 +- 10 files changed, 133 insertions(+), 185 deletions(-) diff --git a/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py b/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py index 620f82f8..710512da 100644 --- a/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py +++ b/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py @@ -3,10 +3,9 @@ from common_module.common_container import CommonContainer from common_module.response_formatter import ResponseFormatter from db_repo_module.models.resource import ResourceScope -from db_repo_module.models.role import Role -from db_repo_module.repositories.sql_alchemy_repository import SQLAlchemyRepository from user_management_module.user_container import UserContainer from user_management_module.services.user_service import UserService +from user_management_module.utils.user_utils import check_is_admin from dependency_injector.wiring import inject from dependency_injector.wiring import Provide from fastapi import Depends @@ -19,20 +18,6 @@ superset_controller = APIRouter() -@inject -async def check_is_admin( - role_id: str, - role_repository: SQLAlchemyRepository[Role] = Depends( - Provide[AuthContainer.role_repository] - ), -) -> bool: - role = await role_repository.find_one(id=role_id) - if not role: - return False - - return role.name == 'admin' - - @superset_controller.get('/v1/superset/authenticate') @inject async def superset_authenticator( @@ -53,7 +38,7 @@ async def superset_authenticator( is_admin = await check_is_admin(role_id) dashboards = await user_service.get_user_resources( - user_id=user_id, scope=ResourceScope.DASHBOARD + user_id=user_id, scope=ResourceScope.DASHBOARD, is_admin=is_admin ) if not dashboards: @@ -68,14 +53,6 @@ async def superset_authenticator( user_id=user_id, scope=ResourceScope.DATA ) - if not is_admin and not data_filters: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=response_formatter.buildErrorResponse( - 'User does not have access to any dashboard' - ), - ) - if data_filters and len(data_filters) < 1: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, diff --git a/wavefront/server/modules/llm_inference_config_module/llm_inference_config_module/controllers/llm_inference_config_controller.py b/wavefront/server/modules/llm_inference_config_module/llm_inference_config_module/controllers/llm_inference_config_controller.py index 1be313d8..d1b42243 100644 --- a/wavefront/server/modules/llm_inference_config_module/llm_inference_config_module/controllers/llm_inference_config_controller.py +++ b/wavefront/server/modules/llm_inference_config_module/llm_inference_config_module/controllers/llm_inference_config_controller.py @@ -1,10 +1,7 @@ import uuid -from auth_module.auth_container import AuthContainer from common_module.common_container import CommonContainer from common_module.response_formatter import ResponseFormatter -from db_repo_module.models.role import Role -from db_repo_module.repositories.sql_alchemy_repository import SQLAlchemyRepository from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends, Request, status from fastapi.responses import JSONResponse @@ -18,26 +15,11 @@ from llm_inference_config_module.services.llm_inference_config_service import ( LlmInferenceConfigService, ) -from user_management_module.constants.auth import SERVICE_AUTH_ROLE_ID +from user_management_module.utils.user_utils import check_is_admin llm_inference_config_router = APIRouter() -@inject -async def check_admin( - role_id: str, - role_repository: SQLAlchemyRepository[Role] = Depends( - Provide[AuthContainer.role_repository] - ), -) -> bool: - if role_id == SERVICE_AUTH_ROLE_ID: - return True - role = await role_repository.find_one(id=role_id) - if not role: - return False - return role.name == 'admin' - - @llm_inference_config_router.post('/v1/llm-inference-configs') @inject async def create_llm_inference_config( @@ -51,7 +33,7 @@ async def create_llm_inference_config( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -99,7 +81,7 @@ async def get_llm_inference_configs( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -169,7 +151,7 @@ async def update_llm_inference_config( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -293,7 +275,7 @@ async def delete_llm_inference_config( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, diff --git a/wavefront/server/modules/plugins_module/plugins_module/controllers/authenticator_controller.py b/wavefront/server/modules/plugins_module/plugins_module/controllers/authenticator_controller.py index 98f23071..8c991c79 100644 --- a/wavefront/server/modules/plugins_module/plugins_module/controllers/authenticator_controller.py +++ b/wavefront/server/modules/plugins_module/plugins_module/controllers/authenticator_controller.py @@ -19,7 +19,7 @@ enable_authenticator, disable_authenticator, ) -from plugins_module.services.datasource_services import check_admin +from user_management_module.utils.user_utils import check_is_admin authenticator_router = APIRouter() @@ -52,7 +52,7 @@ async def create_authenticator( """Create a new authenticator configuration.""" role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -105,7 +105,7 @@ async def get_all_authenticators_endpoint( """Get all authenticator configurations.""" role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -145,7 +145,7 @@ async def get_authenticator( """Get authenticator configuration by ID.""" role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -202,7 +202,7 @@ async def update_authenticator( """Update authenticator configuration.""" role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -264,7 +264,7 @@ async def delete_authenticator( """Delete authenticator configuration.""" role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -305,7 +305,7 @@ async def enable_authenticator_endpoint( """Enable an authenticator.""" role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -346,7 +346,7 @@ async def disable_authenticator_endpoint( """Disable an authenticator.""" role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, diff --git a/wavefront/server/modules/plugins_module/plugins_module/controllers/datasource_controller.py b/wavefront/server/modules/plugins_module/plugins_module/controllers/datasource_controller.py index 7c1c7ffa..e22788e9 100644 --- a/wavefront/server/modules/plugins_module/plugins_module/controllers/datasource_controller.py +++ b/wavefront/server/modules/plugins_module/plugins_module/controllers/datasource_controller.py @@ -21,7 +21,6 @@ from datasource import DatasourcePlugin from datasource.types import DataSourceType, QueryResult, TableListResult from plugins_module.services.datasource_services import ( - check_admin, check_is_valid_resource, fetch_data_filters, get_datasource_config, @@ -37,6 +36,7 @@ from user_management_module.services.user_service import UserService from flo_cloud.cloud_storage import CloudStorageManager from fastapi import HTTPException +from user_management_module.utils.user_utils import check_is_admin from user_management_module.utils.user_utils import get_current_user from plugins_module.services.dynamic_query_service import DynamicQueryService from db_repo_module.cache.cache_manager import CacheManager @@ -70,7 +70,7 @@ async def add_datasource( ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -141,7 +141,7 @@ async def update_datasource( ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -244,7 +244,7 @@ async def delete_datasource( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( @@ -290,7 +290,7 @@ async def get_datasources( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -319,7 +319,7 @@ async def get_datasource( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -350,7 +350,7 @@ async def test_datasource_connection( ), ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -385,7 +385,7 @@ async def get_tables( ): role_id = request.state.session.role_id - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, @@ -448,7 +448,7 @@ async def query_datasource( rls_filters = [] filter = query_filter - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: rls_filters = await user_service.get_user_resources( user_id=user_id, scope=ResourceScope.DATA @@ -551,7 +551,7 @@ async def create_dynamic_query( ), ): role_id, _, _ = get_current_user(request) - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: raise HTTPException(status_code=401, detail='Unauthorized') @@ -588,7 +588,7 @@ async def get_all_dynamic_query_yaml( ), ): role_id, _, _ = get_current_user(request) - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: raise HTTPException(status_code=401, detail='Unauthorized') @@ -621,7 +621,7 @@ async def get_dynamic_query( ), ): role_id, _, _ = get_current_user(request) - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: raise HTTPException(status_code=401, detail='Unauthorized') @@ -679,7 +679,7 @@ async def execute_dynamic_query( yaml_query, _ = await dynamic_query_yaml_service.get_dynamic_yaml_query(query_id) rls_filter_str = None - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: rls_filters = await user_service.get_user_resources( user_id=user_id, scope=ResourceScope.DATA @@ -787,7 +787,7 @@ async def export_dynamic_query_csv( ) rls_filter_str = None - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: rls_filters = await user_service.get_user_resources( user_id=user_id, scope=ResourceScope.DATA @@ -919,7 +919,7 @@ async def delete_dynamic_query( ), ): role_id, _, _ = get_current_user(request) - is_admin = await check_admin(role_id) + is_admin = await check_is_admin(role_id) if not is_admin: raise HTTPException(status_code=401, detail='Unauthorized') await dynamic_query_yaml_service.delete_dynamic_query(datasource_id, query_id) diff --git a/wavefront/server/modules/plugins_module/plugins_module/services/datasource_services.py b/wavefront/server/modules/plugins_module/plugins_module/services/datasource_services.py index a6eceb6e..c88d39bb 100644 --- a/wavefront/server/modules/plugins_module/plugins_module/services/datasource_services.py +++ b/wavefront/server/modules/plugins_module/plugins_module/services/datasource_services.py @@ -1,15 +1,11 @@ import collections from datasource import DataSourceType, BigQueryConfig, RedshiftConfig, PostgresConfig from db_repo_module.models.datasource import Datasource -from db_repo_module.models.role import Role from db_repo_module.repositories.sql_alchemy_repository import SQLAlchemyRepository -from dependency_injector.wiring import inject from dependency_injector.wiring import Provide from fastapi import Depends -from auth_module.auth_container import AuthContainer from plugins_module.plugins_container import PluginsContainer from plugins_module.utils.helper import AddDatasourcePayload -from user_management_module.constants.auth import SERVICE_AUTH_ROLE_ID async def get_datasource_config( @@ -34,21 +30,6 @@ async def get_datasource_config( raise ValueError(f'Invalid datasource type: {datasource.type}') -@inject -async def check_admin( - role_id: str, - role_repositroy: SQLAlchemyRepository[Role] = Depends( - Provide(AuthContainer.role_repository) - ), -) -> bool: - if role_id == SERVICE_AUTH_ROLE_ID: - return True - role = await role_repositroy.find_one(id=role_id) - if not role: - return False - return role.name == 'admin' - - def check_is_valid_resource(resource_id: str) -> bool: if resource_id in [ 'parsed_data_object', diff --git a/wavefront/server/modules/user_management_module/user_management_module/constants/auth.py b/wavefront/server/modules/user_management_module/user_management_module/constants/auth.py index 27c40f5c..d0ce4848 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/constants/auth.py +++ b/wavefront/server/modules/user_management_module/user_management_module/constants/auth.py @@ -9,3 +9,5 @@ class RootfloHeaders: SERVICE_AUTH_ROLE_ID = 'floconsole-service' + +ADMIN_ROLE_NAME = 'admin' diff --git a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py index f223dd9a..48af8999 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py +++ b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py @@ -7,7 +7,6 @@ from db_repo_module.models.resource import ResourceScope from db_repo_module.models.role import Role from db_repo_module.models.role_resource import RoleResource -from db_repo_module.models.user_role import UserRole from db_repo_module.repositories.sql_alchemy_repository import SQLAlchemyRepository from dependency_injector.wiring import inject from dependency_injector.wiring import Provide @@ -49,9 +48,6 @@ async def create_resource( role_resource_repository: SQLAlchemyRepository[RoleResource] = Depends( Provide[UserContainer.role_resource_repository] ), - user_role_repository: SQLAlchemyRepository[UserRole] = Depends( - Provide[UserContainer.user_role_repository] - ), ): user_role_id, _, _ = get_current_user(request) is_admin = await check_is_admin(user_role_id) @@ -100,21 +96,6 @@ async def create_resource( await role_resource_repository.create_all( role_resources, replace=True, session=session ) - admin_users = await user_role_repository.find( - role_id=user_role_id, session=session - ) - - permissions: list[UserRole] = [] - if admin_users and len(admin_users) > 0: - for user in admin_users: - for role in roles: - permissions.append( - UserRole(user_id=user.user_id, role_id=role.id) - ) - - await user_role_repository.create_all( - permissions, replace=True, session=session - ) await session.commit() @@ -226,9 +207,12 @@ async def get_resource( description='The scopes of the resources to fetch', ), ): - _, user_id, _ = get_current_user(request) + role_id, user_id, _ = get_current_user(request) + is_admin = await check_is_admin(role_id) - resources = await user_service.get_user_resources(user_id=user_id, scopes=scopes) + resources = await user_service.get_user_resources( + user_id=user_id, scopes=scopes, is_admin=is_admin + ) data = [res.to_dict() for res in resources] diff --git a/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py b/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py index 25d28b09..5f2710bb 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py +++ b/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py @@ -66,9 +66,6 @@ async def create_user( user_repository: SQLAlchemyRepository[User] = Depends( Provide[UserContainer.user_repository] ), - role_repository: SQLAlchemyRepository[Role] = Depends( - Provide[UserContainer.role_repository] - ), user_service: UserService = Depends(Provide[UserContainer.user_service]), cache_manager: CacheManager = Depends(Provide[UserContainer.cache_manager]), ): @@ -81,6 +78,8 @@ async def create_user( content=response_formatter.buildErrorResponse('Access denied'), ) + is_creating_admin = role_id in new_user.role_id + existing_user = await user_repository.find_one(email=new_user.email) if existing_user: if existing_user.deleted: @@ -109,25 +108,26 @@ async def create_user( async with user_repository.session() as session: try: - get_console_resources_query = ( - select(Resource) - .join(RoleResource, Resource.id == RoleResource.resource_id) - .where( - and_( - RoleResource.role_id.in_(new_user.role_id), - Resource.scope == ResourceScope.CONSOLE, + if not is_creating_admin: + get_console_resources_query = ( + select(Resource) + .join(RoleResource, Resource.id == RoleResource.resource_id) + .where( + and_( + RoleResource.role_id.in_(new_user.role_id), + Resource.scope == ResourceScope.CONSOLE, + ) ) ) - ) - result = await session.execute(get_console_resources_query) - console_resources = result.scalars().all() - if len(console_resources) == 0: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=response_formatter.buildErrorResponse( - 'Atleast one console resource is mandatory' - ), - ) + result = await session.execute(get_console_resources_query) + console_resources = result.scalars().all() + if len(console_resources) == 0: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=response_formatter.buildErrorResponse( + 'Atleast one console resource is mandatory' + ), + ) hashed_password = hash_password(new_user.password) user = User( @@ -158,16 +158,9 @@ async def create_user( await session.flush() user_id = user.id - if role_id in new_user.role_id: # Is creating admin user - all_roles = await role_repository.find() - user_roles = [ - UserRole(user_id=user_id, role_id=role.id) for role in all_roles - ] - else: # Is creating user with role other than admin - user_roles = [ - UserRole(user_id=user_id, role_id=role_id) - for role_id in new_user.role_id - ] + user_roles = [ + UserRole(user_id=user_id, role_id=r_id) for r_id in new_user.role_id + ] session.add_all(user_roles) await session.commit() diff --git a/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py b/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py index 149b2cf1..bf9ed6f2 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py +++ b/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py @@ -8,6 +8,7 @@ from db_repo_module.models.role_resource import RoleResource from db_repo_module.cache.cache_manager import CacheManager from sqlalchemy import select, Result, and_ +from user_management_module.constants.auth import ADMIN_ROLE_NAME from common_module.response_formatter import ResponseFormatter from common_module.log.logger import logger from user_management_module.utils.password_utils import hash_password @@ -36,28 +37,38 @@ async def get_user_resources( user_id: str, scope: Optional[ResourceScope] = None, scopes: Optional[List[ResourceScope]] = None, + is_admin: bool = False, ) -> List[Resource]: """ Fetch all resources accessible to a user based on their roles. + Admin access is decided by the caller via the single ``check_is_admin`` + helper and passed in through ``is_admin``. Admins implicitly have access + to every resource, so the role join is skipped for them. + Args: user_id: The ID of the user scope: Single scope to filter by (optional) scopes: Multiple scopes to filter by (optional) + is_admin: Whether the requesting user is an admin Returns: List of Resource objects the user has access to """ async with self.resource_repository.session() as session: - statement = ( - select(Resource) - .join(RoleResource, Resource.id == RoleResource.resource_id) - .join(Role, Role.id == RoleResource.role_id) - .join(UserRole, UserRole.role_id == Role.id) - .join(User, UserRole.user_id == User.id) - .where(UserRole.user_id == user_id) - .where(User.deleted.is_(False)) - ) + if is_admin: + # Admins have access to all resources — skip the role join + statement = select(Resource) + else: + statement = ( + select(Resource) + .join(RoleResource, Resource.id == RoleResource.resource_id) + .join(Role, Role.id == RoleResource.role_id) + .join(UserRole, UserRole.role_id == Role.id) + .join(User, UserRole.user_id == User.id) + .where(UserRole.user_id == user_id) + .where(User.deleted.is_(False)) + ) # Apply scope filtering if scope is not None: @@ -73,6 +84,8 @@ async def get_user_role_for_scope( ) -> Optional[str]: """ Get the user's role ID for a specific resource scope. + Admin users are granted access to every scope and their admin role_id is + returned directly without checking resource assignments. Args: user_id: The ID of the user @@ -82,6 +95,22 @@ async def get_user_role_for_scope( The role_id if user has access to the scope, None otherwise """ async with self.resource_repository.session() as session: + # Admins have access to all scopes; return their role_id immediately. + # role_id is not yet known at login, so admin status is resolved by + # user_id here (the one place this lookup is unavoidable). + admin_stmt = ( + select(UserRole.role_id) + .join(Role, UserRole.role_id == Role.id) + .join(User, UserRole.user_id == User.id) + .where(UserRole.user_id == user_id) + .where(User.deleted.is_(False)) + .where(Role.name == ADMIN_ROLE_NAME) + ) + admin_result = await session.execute(admin_stmt) + admin_role_id = admin_result.scalar() + if admin_role_id: + return str(admin_role_id) + statement = ( select(UserRole.role_id) .join(Role, UserRole.role_id == Role.id) @@ -118,6 +147,8 @@ async def reactivate_user( current_admin_role_id: str, response_formatter: ResponseFormatter, ) -> JSONResponse: + is_reactivating_admin = current_admin_role_id in new_user_data.role_id + try: async with self.user_repository.session() as session: # Validate roles first @@ -135,26 +166,28 @@ async def reactivate_user( ), ) - # Validate console resource requirement - console_resources_query = ( - select(Resource) - .join(RoleResource, Resource.id == RoleResource.resource_id) - .where( - and_( - RoleResource.role_id.in_(new_user_data.role_id), - Resource.scope == ResourceScope.CONSOLE, + # Admins have implicit access to all resources; only validate console + # resource requirement for non-admin users + if not is_reactivating_admin: + console_resources_query = ( + select(Resource) + .join(RoleResource, Resource.id == RoleResource.resource_id) + .where( + and_( + RoleResource.role_id.in_(new_user_data.role_id), + Resource.scope == ResourceScope.CONSOLE, + ) ) ) - ) - console_result = await session.execute(console_resources_query) - console_resources = console_result.scalars().all() - if len(console_resources) == 0: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content=response_formatter.buildErrorResponse( - 'Atleast one console resource is mandatory' - ), - ) + console_result = await session.execute(console_resources_query) + console_resources = console_result.scalars().all() + if len(console_resources) == 0: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=response_formatter.buildErrorResponse( + 'Atleast one console resource is mandatory' + ), + ) user_updates = { 'deleted': False, @@ -179,21 +212,10 @@ async def reactivate_user( ), ) - # Handle role assignments - if ( - current_admin_role_id in new_user_data.role_id - ): # Is creating admin user - all_roles = await session.execute(select(Role)) - all_roles_list = all_roles.scalars().all() - user_roles = [ - UserRole(user_id=existing_user.id, role_id=role.id) - for role in all_roles_list - ] - else: # Is creating user with specific roles - user_roles = [ - UserRole(user_id=existing_user.id, role_id=role_id) - for role_id in new_user_data.role_id - ] + user_roles = [ + UserRole(user_id=existing_user.id, role_id=role_id) + for role_id in new_user_data.role_id + ] session.add_all(user_roles) await session.commit() diff --git a/wavefront/server/modules/user_management_module/user_management_module/utils/user_utils.py b/wavefront/server/modules/user_management_module/user_management_module/utils/user_utils.py index cd93562d..6351ecfd 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/utils/user_utils.py +++ b/wavefront/server/modules/user_management_module/user_management_module/utils/user_utils.py @@ -12,6 +12,8 @@ from fastapi import status from fastapi.params import Depends from fastapi.responses import JSONResponse +from user_management_module.constants.auth import ADMIN_ROLE_NAME +from user_management_module.constants.auth import SERVICE_AUTH_ROLE_ID from user_management_module.services.account_lockout_service import ( AccountLockoutService, ) @@ -35,14 +37,19 @@ async def check_is_admin( Provide[UserContainer.role_repository] ), ) -> bool: - if role_id == 'floconsole-service': + """Single source of truth for admin checks based on the session role_id. + + Service identities (mTLS/passthrough/service-to-service) carry the + SERVICE_AUTH_ROLE_ID and are always treated as admins. + """ + if role_id == SERVICE_AUTH_ROLE_ID: return True role = await role_repository.find_one(id=role_id) if not role: return False - return role.name == 'admin' + return role.name == ADMIN_ROLE_NAME def create_account_lockout_response( From 1f819c20a2037479bad67aa9577cb931f6a6e59b Mon Sep 17 00:00:00 2001 From: vishnu r kumar Date: Mon, 15 Jun 2026 14:22:02 +0530 Subject: [PATCH 2/5] fix: make gmail email automate optional at initiation --- .../user_management_module/services/email_service.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wavefront/server/modules/user_management_module/user_management_module/services/email_service.py b/wavefront/server/modules/user_management_module/user_management_module/services/email_service.py index d8555753..3cda9286 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/services/email_service.py +++ b/wavefront/server/modules/user_management_module/user_management_module/services/email_service.py @@ -141,11 +141,22 @@ def __init__(self, client_id, client_secret, refresh_token, email_sender): self.scopes = ['https://www.googleapis.com/auth/gmail.send'] if not all([client_id, client_secret, refresh_token, email_sender]): + logger.warning( + 'Gmail OAuth credentials are incomplete ' + '(client_id, client_secret, refresh_token, email_sender). ' + 'Email sending will fail if attempted.' + ) + + def _assert_credentials(self): + if not all( + [self.client_id, self.client_secret, self.refresh_token, self.email_sender] + ): raise Exception( 'Gmail OAuth requires client_id, client_secret, refresh_token, and email_sender' ) def get_credentials(self) -> Credentials: + self._assert_credentials() creds = Credentials( token=None, refresh_token=self.refresh_token, From 46bb947e7c4f7b0ad0bdde93a952f033abdc7f14 Mon Sep 17 00:00:00 2001 From: vishnu r kumar Date: Mon, 15 Jun 2026 15:29:19 +0530 Subject: [PATCH 3/5] chore: move access to whomai and add pagination for roles & resources --- .../controllers/superset_controller.py | 9 +- .../controllers/access_controller.py | 60 +++++++++- .../controllers/user_controller.py | 36 +++++- .../services/user_service.py | 110 ++++++++++++++---- 4 files changed, 182 insertions(+), 33 deletions(-) diff --git a/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py b/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py index 710512da..0c53599e 100644 --- a/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py +++ b/wavefront/server/modules/auth_module/auth_module/controllers/superset_controller.py @@ -37,9 +37,12 @@ async def superset_authenticator( data_filters = [] is_admin = await check_is_admin(role_id) - dashboards = await user_service.get_user_resources( - user_id=user_id, scope=ResourceScope.DASHBOARD, is_admin=is_admin - ) + if is_admin: + dashboards = await user_service.get_all_resources(scope=ResourceScope.DASHBOARD) + else: + dashboards = await user_service.get_user_resources( + user_id=user_id, scope=ResourceScope.DASHBOARD + ) if not dashboards: return JSONResponse( diff --git a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py index 48af8999..0f326a4b 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py +++ b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py @@ -17,6 +17,7 @@ from fastapi.params import Depends from fastapi.responses import JSONResponse from sqlalchemy import func +from sqlalchemy import or_ from sqlalchemy import Result from sqlalchemy import select from sqlalchemy.orm import selectinload @@ -206,19 +207,33 @@ async def get_resource( default=[ResourceScope.DASHBOARD, ResourceScope.CONSOLE], description='The scopes of the resources to fetch', ), + search: Optional[str] = Query( + None, description='Search by key, value or description' + ), + limit: int = Query(100, description='Maximum number of resources to return'), + offset: int = Query(0, description='Number of resources to skip'), ): - role_id, user_id, _ = get_current_user(request) + role_id, _, _ = get_current_user(request) is_admin = await check_is_admin(role_id) - resources = await user_service.get_user_resources( - user_id=user_id, scopes=scopes, is_admin=is_admin + if not is_admin: + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content=response_formatter.buildErrorResponse('Access denied'), + ) + + resources = await user_service.get_all_resources( + scopes=scopes, search=search, offset=offset, limit=limit ) + total = await user_service.count_all_resources(scopes=scopes, search=search) data = [res.to_dict() for res in resources] return JSONResponse( status_code=status.HTTP_200_OK, - content=response_formatter.buildSuccessResponse(data={'resources': data}), + content=response_formatter.buildSuccessResponse( + data={'resources': data, 'total': total} + ), ) @@ -236,6 +251,9 @@ async def get_role( default=[ResourceScope.CONSOLE], description='The scopes of the roles to fetch' ), select_item: Optional[str] = None, + search: Optional[str] = Query(None, description='Search by name or description'), + limit: int = Query(100, description='Maximum number of roles to return'), + offset: int = Query(0, description='Number of roles to skip'), ): role_id, _, _ = get_current_user(request) is_admin = await check_is_admin(role_id) @@ -257,9 +275,22 @@ async def get_role( ) valid_columns.append(getattr(Role, item)) + search_filter = None + if search and search.strip(): + term = f'%{search.strip()}%' + search_filter = or_(Role.name.ilike(term), Role.description.ilike(term)) + async with role_repository.session() as session: if valid_columns: statement = select(Role).options(selectinload(Role.resources)) + count_statement = select(func.count()).select_from(Role) + if search_filter is not None: + statement = statement.where(search_filter) + count_statement = count_statement.where(search_filter) + + total = (await session.execute(count_statement)).scalar() or 0 + + statement = statement.offset(offset).limit(limit) result = await session.execute(statement) roles = result.scalars().unique().all() data = [] @@ -279,15 +310,32 @@ async def get_role( .join(RoleResource, Role.id == RoleResource.role_id) .join(Resource, Resource.id == RoleResource.resource_id) .where(Resource.scope.in_(scopes)) + .distinct() + ) + count_statement = ( + select(func.count(func.distinct(Role.id))) + .select_from(Role) + .join(RoleResource, Role.id == RoleResource.role_id) + .join(Resource, Resource.id == RoleResource.resource_id) + .where(Resource.scope.in_(scopes)) ) + if search_filter is not None: + statement = statement.where(search_filter) + count_statement = count_statement.where(search_filter) + + total = (await session.execute(count_statement)).scalar() or 0 + + statement = statement.offset(offset).limit(limit) result: Result = await session.execute(statement) - roles = result.scalars().all() + roles = result.scalars().unique().all() data = [role.to_dict() for role in roles] return JSONResponse( status_code=status.HTTP_200_OK, - content=response_formatter.buildSuccessResponse(data={'roles': data}), + content=response_formatter.buildSuccessResponse( + data={'roles': data, 'total': total} + ), ) diff --git a/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py b/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py index 5f2710bb..1775c8ea 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py +++ b/wavefront/server/modules/user_management_module/user_management_module/controllers/user_controller.py @@ -547,8 +547,9 @@ async def get_resources( user_repository: SQLAlchemyRepository[User] = Depends( Provide[UserContainer.user_repository] ), + user_service: UserService = Depends(Provide[UserContainer.user_service]), ): - _, user_id, _ = get_current_user(request) + role_id, user_id, _ = get_current_user(request) user = await user_repository.find_one(id=user_id) if not user: @@ -557,9 +558,40 @@ async def get_resources( content=response_formatter.buildErrorResponse('User not found'), ) + is_admin = await check_is_admin(role_id) + + # Console resources are the user's UI identity marker (e.g. admin_resource), + # so they always stay role-based. + console_resources = await user_service.get_user_resources( + user_id=user_id, scope=ResourceScope.CONSOLE + ) + + # Admins have implicit access to every dashboard, so they receive the full + # list; non-admins only get the dashboards their roles grant. + if is_admin: + dashboards: List[Resource] = await user_service.get_all_resources( + scope=ResourceScope.DASHBOARD + ) + data: List[Resource] = [] + else: + dashboards = await user_service.get_user_resources( + user_id=user_id, scope=ResourceScope.DASHBOARD + ) + data = await user_service.get_user_resources( + user_id=user_id, scope=ResourceScope.DATA + ) + + resource = { + 'console_resources': [res.to_dict() for res in console_resources], + 'dashboards': [res.to_dict() for res in dashboards], + 'data': [res.to_dict() for res in data], + } + return JSONResponse( status_code=status.HTTP_200_OK, - content=response_formatter.buildSuccessResponse({'user': user.to_dict()}), + content=response_formatter.buildSuccessResponse( + {'user': user.to_dict(), 'resource': resource} + ), ) diff --git a/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py b/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py index bf9ed6f2..49ed8743 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py +++ b/wavefront/server/modules/user_management_module/user_management_module/services/user_service.py @@ -7,7 +7,7 @@ from db_repo_module.models.role import Role from db_repo_module.models.role_resource import RoleResource from db_repo_module.cache.cache_manager import CacheManager -from sqlalchemy import select, Result, and_ +from sqlalchemy import select, Result, and_, or_, func from user_management_module.constants.auth import ADMIN_ROLE_NAME from common_module.response_formatter import ResponseFormatter from common_module.log.logger import logger @@ -37,40 +37,29 @@ async def get_user_resources( user_id: str, scope: Optional[ResourceScope] = None, scopes: Optional[List[ResourceScope]] = None, - is_admin: bool = False, ) -> List[Resource]: """ - Fetch all resources accessible to a user based on their roles. - - Admin access is decided by the caller via the single ``check_is_admin`` - helper and passed in through ``is_admin``. Admins implicitly have access - to every resource, so the role join is skipped for them. + Fetch all resources a user has access to through their role assignments. Args: user_id: The ID of the user scope: Single scope to filter by (optional) scopes: Multiple scopes to filter by (optional) - is_admin: Whether the requesting user is an admin Returns: List of Resource objects the user has access to """ async with self.resource_repository.session() as session: - if is_admin: - # Admins have access to all resources — skip the role join - statement = select(Resource) - else: - statement = ( - select(Resource) - .join(RoleResource, Resource.id == RoleResource.resource_id) - .join(Role, Role.id == RoleResource.role_id) - .join(UserRole, UserRole.role_id == Role.id) - .join(User, UserRole.user_id == User.id) - .where(UserRole.user_id == user_id) - .where(User.deleted.is_(False)) - ) + statement = ( + select(Resource) + .join(RoleResource, Resource.id == RoleResource.resource_id) + .join(Role, Role.id == RoleResource.role_id) + .join(UserRole, UserRole.role_id == Role.id) + .join(User, UserRole.user_id == User.id) + .where(UserRole.user_id == user_id) + .where(User.deleted.is_(False)) + ) - # Apply scope filtering if scope is not None: statement = statement.where(Resource.scope == scope) elif scopes is not None: @@ -79,6 +68,83 @@ async def get_user_resources( result: Result = await session.execute(statement) return result.scalars().all() + def _resource_filters( + self, + scope: Optional[ResourceScope] = None, + scopes: Optional[List[ResourceScope]] = None, + search: Optional[str] = None, + ) -> list: + """Build the WHERE conditions shared by resource listing and counting.""" + conditions: list = [] + if scope is not None: + conditions.append(Resource.scope == scope) + elif scopes is not None: + conditions.append(Resource.scope.in_(scopes)) + + if search and search.strip(): + term = f'%{search.strip()}%' + conditions.append( + or_( + Resource.key.ilike(term), + Resource.value.ilike(term), + Resource.description.ilike(term), + ) + ) + return conditions + + async def get_all_resources( + self, + scope: Optional[ResourceScope] = None, + scopes: Optional[List[ResourceScope]] = None, + search: Optional[str] = None, + offset: int = 0, + limit: Optional[int] = None, + ) -> List[Resource]: + """ + Fetch every resource in the system, optionally filtered by scope/search. + + Used to grant admins implicit access to all resources without requiring + explicit role assignments, and to power the admin resource listing. + + Args: + scope: Single scope to filter by (optional) + scopes: Multiple scopes to filter by (optional) + search: Case-insensitive term matched against key/value/description + offset: Number of records to skip (pagination) + limit: Maximum number of records to return (no limit when None) + + Returns: + List of all matching Resource objects + """ + async with self.resource_repository.session() as session: + statement = select(Resource) + conditions = self._resource_filters(scope, scopes, search) + if conditions: + statement = statement.where(and_(*conditions)) + + statement = statement.offset(offset) + if limit is not None: + statement = statement.limit(limit) + + result: Result = await session.execute(statement) + return result.scalars().all() + + async def count_all_resources( + self, + scope: Optional[ResourceScope] = None, + scopes: Optional[List[ResourceScope]] = None, + search: Optional[str] = None, + ) -> int: + """Total number of resources matching the given scope/search filters.""" + async with self.resource_repository.session() as session: + statement = select(func.count()).select_from(Resource) + conditions = self._resource_filters(scope, scopes, search) + if conditions: + statement = statement.where(and_(*conditions)) + + result: Result = await session.execute(statement) + return result.scalar() or 0 + async def get_user_role_for_scope( self, user_id: str, scope: ResourceScope ) -> Optional[str]: From 647080ae85febf689b666527f0c523efdca83de7 Mon Sep 17 00:00:00 2001 From: vishnu r kumar Date: Mon, 15 Jun 2026 15:45:18 +0530 Subject: [PATCH 4/5] chore: fetch all resource types by default --- .../controllers/access_controller.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py index 0f326a4b..5ebc1883 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py +++ b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py @@ -203,14 +203,16 @@ async def get_resource( Provide[CommonContainer.response_formatter] ), user_service: UserService = Depends(Provide[UserContainer.user_service]), - scopes: list[str] = Query( - default=[ResourceScope.DASHBOARD, ResourceScope.CONSOLE], - description='The scopes of the resources to fetch', + scopes: Optional[list[str]] = Query( + default=None, + description='Scopes of the resources to fetch (all scopes when omitted)', ), search: Optional[str] = Query( None, description='Search by key, value or description' ), - limit: int = Query(100, description='Maximum number of resources to return'), + limit: Optional[int] = Query( + None, description='Maximum number of resources to return (all when omitted)' + ), offset: int = Query(0, description='Number of resources to skip'), ): role_id, _, _ = get_current_user(request) @@ -252,7 +254,9 @@ async def get_role( ), select_item: Optional[str] = None, search: Optional[str] = Query(None, description='Search by name or description'), - limit: int = Query(100, description='Maximum number of roles to return'), + limit: Optional[int] = Query( + None, description='Maximum number of roles to return (all when omitted)' + ), offset: int = Query(0, description='Number of roles to skip'), ): role_id, _, _ = get_current_user(request) @@ -290,7 +294,9 @@ async def get_role( total = (await session.execute(count_statement)).scalar() or 0 - statement = statement.offset(offset).limit(limit) + statement = statement.offset(offset) + if limit is not None: + statement = statement.limit(limit) result = await session.execute(statement) roles = result.scalars().unique().all() data = [] @@ -325,7 +331,9 @@ async def get_role( total = (await session.execute(count_statement)).scalar() or 0 - statement = statement.offset(offset).limit(limit) + statement = statement.offset(offset) + if limit is not None: + statement = statement.limit(limit) result: Result = await session.execute(statement) roles = result.scalars().unique().all() From 606ef600881effe1c0301a5b1794fe7da3234a57 Mon Sep 17 00:00:00 2001 From: vishnu r kumar Date: Mon, 15 Jun 2026 20:31:07 +0530 Subject: [PATCH 5/5] fix dupliactes in fetch roles --- .../controllers/access_controller.py | 92 ++++++++----------- 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py index 5ebc1883..d1daf526 100644 --- a/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py +++ b/wavefront/server/modules/user_management_module/user_management_module/controllers/access_controller.py @@ -268,75 +268,55 @@ async def get_role( content=response_formatter.buildErrorResponse('Access denied'), ) item_to_select = select_item.split(',') if select_item else [] - valid_columns = [] for item in item_to_select: - if not getattr(Role, item): + if not hasattr(Role, item): return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content=response_formatter.buildErrorResponse( error=f'Invalid column {item}' ), ) - valid_columns.append(getattr(Role, item)) - search_filter = None + # Roles having at least one resource in the requested scopes. The IN-subquery + # keeps the outer query join-free, so each role is returned exactly once. + filters = [ + Role.id.in_( + select(RoleResource.role_id) + .join(Resource, Resource.id == RoleResource.resource_id) + .where(Resource.scope.in_(scopes)) + ) + ] if search and search.strip(): term = f'%{search.strip()}%' - search_filter = or_(Role.name.ilike(term), Role.description.ilike(term)) + filters.append(or_(Role.name.ilike(term), Role.description.ilike(term))) async with role_repository.session() as session: - if valid_columns: - statement = select(Role).options(selectinload(Role.resources)) - count_statement = select(func.count()).select_from(Role) - if search_filter is not None: - statement = statement.where(search_filter) - count_statement = count_statement.where(search_filter) - - total = (await session.execute(count_statement)).scalar() or 0 - - statement = statement.offset(offset) - if limit is not None: - statement = statement.limit(limit) - result = await session.execute(statement) - roles = result.scalars().unique().all() - data = [] - for role in roles: - role_dict = {} - for col in item_to_select: - if col == 'resources': - role_dict[col] = [ - resource.to_dict() for resource in role.resources - ] - else: - role_dict[col] = str(getattr(role, col)) - data.append(role_dict) - else: - statement = ( - select(Role) - .join(RoleResource, Role.id == RoleResource.role_id) - .join(Resource, Resource.id == RoleResource.resource_id) - .where(Resource.scope.in_(scopes)) - .distinct() + total = ( + await session.execute( + select(func.count()).select_from(Role).where(*filters) ) - count_statement = ( - select(func.count(func.distinct(Role.id))) - .select_from(Role) - .join(RoleResource, Role.id == RoleResource.role_id) - .join(Resource, Resource.id == RoleResource.resource_id) - .where(Resource.scope.in_(scopes)) - ) - if search_filter is not None: - statement = statement.where(search_filter) - count_statement = count_statement.where(search_filter) - - total = (await session.execute(count_statement)).scalar() or 0 - - statement = statement.offset(offset) - if limit is not None: - statement = statement.limit(limit) - result: Result = await session.execute(statement) - roles = result.scalars().unique().all() - + ).scalar() or 0 + + statement = select(Role).where(*filters) + if 'resources' in item_to_select: + statement = statement.options(selectinload(Role.resources)) + statement = statement.offset(offset) + if limit is not None: + statement = statement.limit(limit) + + roles = (await session.execute(statement)).scalars().all() + + if item_to_select: + data = [ + { + col: [resource.to_dict() for resource in role.resources] + if col == 'resources' + else str(getattr(role, col)) + for col in item_to_select + } + for role in roles + ] + else: data = [role.to_dict() for role in roles] return JSONResponse(