From 1dc58d7af3968e677a685cdaab3366474e1d4272 Mon Sep 17 00:00:00 2001 From: daeyeon ko Date: Fri, 22 Aug 2025 15:11:33 +0900 Subject: [PATCH 1/4] refactor: remove workspace owner role count check when delete/update role binding Signed-off-by: daeyeon ko --- .../identity/service/role_binding_service.py | 42 +++++-------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/src/spaceone/identity/service/role_binding_service.py b/src/spaceone/identity/service/role_binding_service.py index 3184a88b..02d23fd5 100644 --- a/src/spaceone/identity/service/role_binding_service.py +++ b/src/spaceone/identity/service/role_binding_service.py @@ -163,12 +163,6 @@ def update_role( request_role_type=new_role_vo.role_type, supported_role_type=["WORKSPACE_OWNER", "WORKSPACE_MEMBER"], ) - self.check_last_workspace_owner_role_binding( - rb_vo.user_id, - new_role_vo.role_type, - rb_vo.workspace_id, - rb_vo.domain_id, - ) elif rb_vo.role_type == new_role_vo.role_type: self.check_last_domain_admin_role_binding( rb_vo.user_id, @@ -249,12 +243,12 @@ def delete(self, params: RoleBindingDeleteRequest) -> None: self.check_last_domain_admin_role_binding( rb_vo.user_id, None, rb_vo.domain_id ) - elif rb_vo.role_type == "WORKSPACE_OWNER": - self.check_last_workspace_owner_role_binding( - rb_vo.user_id, None, rb_vo.workspace_id, rb_vo.domain_id - ) - # Update user role type + # Update the user's representative role_type. + # A user's role_type is a single, domain-level field that summarizes their highest-priority role. + # Since deleting a role binding (even a workspace-specific one) might change this representative role, + # we must re-evaluate all of the user's remaining role bindings across the entire domain + # to determine their new, correct role_type. remain_rb_vos = self.role_binding_manager.filter_role_bindings( domain_id=params.domain_id, user_id=rb_vo.user_id ) @@ -417,27 +411,6 @@ def check_last_domain_admin_role_binding( if rb_vos.count() == 1 and new_role_type != "DOMAIN_ADMIN": raise ERROR_LAST_DOMAIN_ADMIN_CANNOT_DELETE() - def check_last_workspace_owner_role_binding( - self, - user_id: str, - new_role_type: Union[str, None], - workspace_id: str, - domain_id: str, - ) -> None: - user_ids = self._get_enabled_user_ids(domain_id) - rb_vos = self.role_binding_manager.filter_role_bindings( - domain_id=domain_id, - workspace_id=workspace_id, - user_id=user_ids, - role_type="WORKSPACE_OWNER", - ) - - if not rb_vos.filter(user_id=user_id): - return None - - if rb_vos.count() == 1 and new_role_type != "WORKSPACE_OWNER": - raise ERROR_LAST_WORKSPACE_OWNER_CANNOT_DELETE() - def _get_enabled_user_ids(self, domain_id: str) -> list: user_vos = self.user_mgr.filter_users( domain_id=domain_id, @@ -453,6 +426,11 @@ def check_self_update_and_delete(requested_user_id: str, user_id: str) -> None: @staticmethod def _get_latest_role_type(before: str, after: str) -> str: + # Determines the user's representative role by comparing priorities (lower number is higher). + # Policy: Workspace-level roles (OWNER, MEMBER) grant permissions within a workspace + # but do not elevate the user's fundamental role at the domain level. + # Therefore, if a user's highest role is a workspace role, their representative + # role_type defaults to 'USER'. Only 'DOMAIN_ADMIN' elevates this status. priority = { "DOMAIN_ADMIN": 1, "WORKSPACE_OWNER": 2, From b543da3fa221618ac858b31456f62870efad1a31 Mon Sep 17 00:00:00 2001 From: daeyeon ko Date: Fri, 22 Aug 2025 17:22:12 +0900 Subject: [PATCH 2/4] refactor: delegate role binding business logic to respective service layers Signed-off-by: daeyeon ko --- .../identity/manager/role_binding_manager.py | 38 ------- src/spaceone/identity/manager/user_manager.py | 68 ++---------- .../identity/service/domain_service.py | 47 ++++++-- .../identity/service/role_binding_service.py | 26 +++++ .../identity/service/system_service.py | 105 ++++++++++++++++-- src/spaceone/identity/service/user_service.py | 90 ++++++++++++++- .../service/workspace_group_service.py | 26 +++++ .../identity/service/workspace_service.py | 27 ++++- 8 files changed, 303 insertions(+), 124 deletions(-) diff --git a/src/spaceone/identity/manager/role_binding_manager.py b/src/spaceone/identity/manager/role_binding_manager.py index b0343b29..822b873e 100644 --- a/src/spaceone/identity/manager/role_binding_manager.py +++ b/src/spaceone/identity/manager/role_binding_manager.py @@ -5,7 +5,6 @@ from spaceone.core.manager import BaseManager from spaceone.identity.manager.user_group_manager import UserGroupManager -from spaceone.identity.manager.workspace_manager import WorkspaceManager from spaceone.identity.model.role_binding.database import RoleBinding _LOGGER = logging.getLogger(__name__) @@ -24,11 +23,6 @@ def _rollback(vo: RoleBinding): role_binding_vo = self.role_binding_model.create(params) self.transaction.add_rollback(_rollback, role_binding_vo) - if role_binding_vo.workspace_id and role_binding_vo.workspace_id != "*": - self._update_workspace_user_count( - role_binding_vo.workspace_id, role_binding_vo.domain_id - ) - return role_binding_vo def update_role_binding_by_vo( @@ -67,11 +61,6 @@ def delete_role_binding_by_vo( {"users": users}, user_group_vo=user_group_vo ) - if role_binding_vo.workspace_id and role_binding_vo.workspace_id != "*": - self._update_workspace_user_count( - role_binding_vo.workspace_id, role_binding_vo.domain_id - ) - def get_role_binding( self, role_binding_id: str, domain_id: str, workspace_id: str = None ) -> RoleBinding: @@ -93,30 +82,3 @@ def list_role_bindings(self, query: dict) -> Tuple[QuerySet, int]: def stat_role_bindings(self, query: dict) -> dict: return self.role_binding_model.stat(**query) - - def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: - workspace_mgr = WorkspaceManager() - - workspace_vo = workspace_mgr.get_workspace(workspace_id, domain_id) - - if workspace_vo and workspace_vo.workspace_id != "*": - user_rb_total_count = self._get_workspace_user_count( - workspace_id, domain_id - ) - - workspace_mgr.update_workspace_by_vo( - {"user_count": user_rb_total_count}, workspace_vo - ) - - def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: - user_rb_ids = self.stat_role_bindings( - query={ - "distinct": "user_id", - "filter": [ - {"k": "workspace_id", "v": workspace_id, "o": "eq"}, - {"k": "domain_id", "v": domain_id, "o": "eq"}, - ], - } - ).get("results", []) - - return len(user_rb_ids) diff --git a/src/spaceone/identity/manager/user_manager.py b/src/spaceone/identity/manager/user_manager.py index 9cd8c61e..d8723b01 100644 --- a/src/spaceone/identity/manager/user_manager.py +++ b/src/spaceone/identity/manager/user_manager.py @@ -4,14 +4,10 @@ import pytz from mongoengine import QuerySet -from spaceone.core.manager import BaseManager +from spaceone.core.manager import BaseManager from spaceone.identity.error.error_user import * from spaceone.identity.lib.cipher import PasswordCipher -from spaceone.identity.manager.project_manager import ProjectManager -from spaceone.identity.manager.role_binding_manager import RoleBindingManager -from spaceone.identity.manager.user_group_manager import UserGroupManager -from spaceone.identity.manager.workspace_group_manager import WorkspaceGroupManager from spaceone.identity.model.user.database import User _LOGGER = logging.getLogger(__name__) @@ -61,7 +57,7 @@ def _rollback(vo: User): def update_user_by_vo(self, params: dict, user_vo: User) -> User: def _rollback(old_data): _LOGGER.info( - f'[update_user_by_vo._rollback] Revert Data: {old_data["user_id"]}' + f"[update_user_by_vo._rollback] Revert Data: {old_data['user_id']}" ) user_vo.update(old_data) @@ -71,7 +67,9 @@ def _rollback(old_data): if email := params.get("email"): self._check_email_format(email) - required_actions = list(params.get("required_actions", user_vo.required_actions or [])) + required_actions = list( + params.get("required_actions", user_vo.required_actions or []) + ) is_change_required_actions = False if new_password := params.get("password"): @@ -96,7 +94,7 @@ def _rollback(old_data): def update_user_password_by_vo(self, user_vo: User, params: dict) -> User: def _rollback(old_data): _LOGGER.info( - f'[update_user_by_vo._rollback] Revert Data: {old_data["user_id"]}' + f"[update_user_by_vo._rollback] Revert Data: {old_data['user_id']}" ) user_vo.update(old_data) @@ -120,59 +118,7 @@ def _rollback(old_data): return user_vo.update(update_params) - @staticmethod - def delete_user_by_vo(user_vo: User) -> None: - rb_mgr = RoleBindingManager() - user_group_mgr = UserGroupManager() - project_mgr = ProjectManager() - workspace_group_mgr = WorkspaceGroupManager() - - # Delete role bindings - rb_vos = rb_mgr.filter_role_bindings( - user_id=user_vo.user_id, domain_id=user_vo.domain_id - ) - for rb_vo in rb_vos: - rb_mgr.delete_role_binding_by_vo(rb_vo) - - # Delete user from user groups - user_group_vos = user_group_mgr.filter_user_groups( - users=user_vo.user_id, domain_id=user_vo.domain_id - ) - for user_group_vo in user_group_vos: - users = user_group_vo.users - users.remove(user_vo.user_id) - user_group_mgr.update_user_group_by_vo( - {"users": users}, user_group_vo=user_group_vo - ) - - # Delete projects - project_vos = project_mgr.filter_projects( - users=user_vo.user_id, domain_id=user_vo.domain_id - ) - for project_vo in project_vos: - users = project_vo.users - users.remove(user_vo.user_id) - project_mgr.update_project_by_vo({"users": users}, project_vo=project_vo) - - # Delete workspace groups - workspace_group_vos = workspace_group_mgr.filter_workspace_groups( - users__user_id=user_vo.user_id, domain_id=user_vo.domain_id - ) - - for workspace_group_vo in workspace_group_vos: - workspace_group_dict = workspace_group_vo.to_mongo().to_dict() - users = workspace_group_dict.get("users", []) - - if users: - updated_users = [ - user for user in users if user.get("user_id") != user_vo.user_id - ] - - if len(updated_users) != len(users): - workspace_group_mgr.update_workspace_group_by_vo( - {"users": updated_users}, workspace_group_vo=workspace_group_vo - ) - + def delete_user(self, user_vo: User) -> None: user_vo.delete() def get_user(self, user_id: str, domain_id: str) -> User: diff --git a/src/spaceone/identity/service/domain_service.py b/src/spaceone/identity/service/domain_service.py index 6d4da054..2d635e3c 100644 --- a/src/spaceone/identity/service/domain_service.py +++ b/src/spaceone/identity/service/domain_service.py @@ -1,22 +1,22 @@ import logging from typing import Union -from spaceone.core.service import * -from spaceone.core.service.utils import * from spaceone.core import utils from spaceone.core.auth.jwt import JWTAuthenticator - -from spaceone.identity.manager.external_auth_manager import ExternalAuthManager +from spaceone.core.service import * +from spaceone.core.service.utils import * +from spaceone.identity.error.error_domain import * +from spaceone.identity.manager.config_manager import ConfigManager from spaceone.identity.manager.domain_manager import DomainManager from spaceone.identity.manager.domain_secret_manager import DomainSecretManager -from spaceone.identity.manager.role_manager import RoleManager +from spaceone.identity.manager.external_auth_manager import ExternalAuthManager from spaceone.identity.manager.role_binding_manager import RoleBindingManager -from spaceone.identity.manager.user_manager import UserManager -from spaceone.identity.manager.config_manager import ConfigManager +from spaceone.identity.manager.role_manager import RoleManager from spaceone.identity.manager.system_manager import SystemManager +from spaceone.identity.manager.user_manager import UserManager +from spaceone.identity.manager.workspace_manager import WorkspaceManager from spaceone.identity.model.domain.request import * from spaceone.identity.model.domain.response import * -from spaceone.identity.error.error_domain import * from spaceone.identity.service.user_service import UserService _LOGGER = logging.getLogger(__name__) @@ -35,6 +35,8 @@ def __init__(self, *args, **kwargs): self.domain_secret_mgr = DomainSecretManager() self.user_mgr = UserManager() self.role_manager = RoleManager() + self.rb_mgr = RoleBindingManager() + self.workspace_mgr = WorkspaceManager() @transaction(permission="identity:Domain.write", role_types=["SYSTEM_ADMIN"]) @convert_model @@ -87,7 +89,9 @@ def create(self, params: DomainCreateRequest) -> Union[DomainResponse, dict]: "domain_id": user_vo.domain_id, "role_type": role_vos[0].role_type, } - role_binding_mgr.create_role_binding(params_rb) + + rb_vo = role_binding_mgr.create_role_binding(params_rb) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) return DomainResponse(**domain_vo.to_dict()) @@ -228,7 +232,7 @@ def get_public_key( root_domain_id = SystemManager.get_root_domain_id() root_pub_jwk = self.domain_secret_mgr.get_domain_public_key(root_domain_id) JWTAuthenticator(root_pub_jwk).validate(token) - except Exception as e: + except Exception: raise ERROR_UNKNOWN(message="Invalid System Token") # Get Public Key from Domain @@ -278,3 +282,26 @@ def stat(self, params: DomainStatQueryRequest) -> dict: query = params.query or {} return self.domain_mgr.stat_domains(query) + + def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: + user_rb_ids = self.rb_mgr.stat_role_bindings( + query={ + "distinct": "user_id", + "filter": [ + {"k": "workspace_id", "v": workspace_id, "o": "eq"}, + {"k": "domain_id", "v": domain_id, "o": "eq"}, + ], + } + ).get("results", []) + return len(user_rb_ids) + + def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) + + if workspace_vo and workspace_vo.workspace_id != "*": + user_rb_total_count = self._get_workspace_user_count( + workspace_id, domain_id + ) + self.workspace_mgr.update_workspace_by_vo( + {"user_count": user_rb_total_count}, workspace_vo + ) diff --git a/src/spaceone/identity/service/role_binding_service.py b/src/spaceone/identity/service/role_binding_service.py index 02d23fd5..0e0deb98 100644 --- a/src/spaceone/identity/service/role_binding_service.py +++ b/src/spaceone/identity/service/role_binding_service.py @@ -115,6 +115,8 @@ def create_role_binding(self, params: dict): # Create role binding rb_vo = self.role_binding_manager.create_role_binding(params) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) + return rb_vo @transaction( @@ -271,6 +273,7 @@ def delete(self, params: RoleBindingDeleteRequest) -> None: self.user_mgr.update_user_by_vo(user_role_info, user_vo) self.role_binding_manager.delete_role_binding_by_vo(rb_vo) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) @transaction( permission="identity:RoleBinding.read", @@ -448,3 +451,26 @@ def _get_latest_role_type(before: str, after: str) -> str: return "USER" return after + + def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: + user_rb_ids = self.role_binding_manager.stat_role_bindings( + query={ + "distinct": "user_id", + "filter": [ + {"k": "workspace_id", "v": workspace_id, "o": "eq"}, + {"k": "domain_id", "v": domain_id, "o": "eq"}, + ], + } + ).get("results", []) + return len(user_rb_ids) + + def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) + + if workspace_vo and workspace_vo.workspace_id != "*": + user_rb_total_count = self._get_workspace_user_count( + workspace_id, domain_id + ) + self.workspace_mgr.update_workspace_by_vo( + {"user_count": user_rb_total_count}, workspace_vo + ) diff --git a/src/spaceone/identity/service/system_service.py b/src/spaceone/identity/service/system_service.py index 120e84f0..a20d2b6b 100644 --- a/src/spaceone/identity/service/system_service.py +++ b/src/spaceone/identity/service/system_service.py @@ -1,20 +1,24 @@ import logging from typing import Union +from spaceone.core.auth.jwt import JWTAuthenticator from spaceone.core.service import * from spaceone.core.service.utils import * -from spaceone.core.auth.jwt import JWTAuthenticator - +from spaceone.identity.error.error_domain import * +from spaceone.identity.error.error_system import * from spaceone.identity.manager.domain_manager import DomainManager from spaceone.identity.manager.domain_secret_manager import DomainSecretManager -from spaceone.identity.manager.role_manager import RoleManager +from spaceone.identity.manager.project_manager import ProjectManager from spaceone.identity.manager.role_binding_manager import RoleBindingManager -from spaceone.identity.manager.user_manager import UserManager +from spaceone.identity.manager.role_manager import RoleManager from spaceone.identity.manager.system_manager import SystemManager +from spaceone.identity.manager.user_group_manager import UserGroupManager +from spaceone.identity.manager.user_manager import UserManager +from spaceone.identity.manager.workspace_group_manager import WorkspaceGroupManager +from spaceone.identity.manager.workspace_manager import WorkspaceManager from spaceone.identity.model.system.request import * from spaceone.identity.model.system.response import * -from spaceone.identity.error.error_system import * -from spaceone.identity.error.error_domain import * +from spaceone.identity.model.user.database import User _LOGGER = logging.getLogger(__name__) @@ -29,6 +33,13 @@ def __init__(self, *args, **kwargs): self.domain_secret_mgr = DomainSecretManager() self.user_mgr = UserManager() self.role_manager = RoleManager() + self.role_binding_manager = RoleBindingManager() + self.workspace_mgr = WorkspaceManager() + self.workspace_group_mgr = WorkspaceGroupManager() + self.user_mgr = UserManager() + self.user_group_mgr = UserGroupManager() + self.project_mgr = ProjectManager() + self.workspace_group_mgr = WorkspaceGroupManager() @transaction() @convert_model @@ -68,7 +79,7 @@ def init(self, params: SystemInitRequest) -> Union[SystemResponse, dict]: root_domain_id ) JWTAuthenticator(root_pub_jwk).validate(token) - except Exception as e: + except Exception: raise ERROR_UNKNOWN(message="Invalid System Token") root_domain_vo = root_domain_vos[0] @@ -85,7 +96,7 @@ def init(self, params: SystemInitRequest) -> Union[SystemResponse, dict]: _LOGGER.debug( f"[init] Delete existing user in root domain: {user_vo.user_id}" ) - self.user_mgr.delete_user_by_vo(user_vo) + self.delete_user_by_vo(user_vo) # create admin user _LOGGER.debug(f"[init] Create admin user: {params.admin.user_id}") @@ -118,7 +129,8 @@ def init(self, params: SystemInitRequest) -> Union[SystemResponse, dict]: "domain_id": user_vo.domain_id, "role_type": role_vos[0].role_type, } - role_binding_mgr.create_role_binding(params_rb) + rb_vo = role_binding_mgr.create_role_binding(params_rb) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) system_token = system_mgr.create_system_token( root_domain_vo.domain_id, user_vo.user_id @@ -131,3 +143,78 @@ def init(self, params: SystemInitRequest) -> Union[SystemResponse, dict]: } return SystemResponse(**response) + + def delete_user_by_vo(self, user_vo: User) -> None: + # Delete role bindings + rb_vos = self.role_binding_manager.filter_role_bindings( + user_id=user_vo.user_id, domain_id=user_vo.domain_id + ) + for rb_vo in rb_vos: + self.role_binding_manager.delete_role_binding_by_vo(rb_vo) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) + + # Delete user from user groups + user_group_vos = self.user_group_mgr.filter_user_groups( + users=user_vo.user_id, domain_id=user_vo.domain_id + ) + for user_group_vo in user_group_vos: + users = user_group_vo.users + users.remove(user_vo.user_id) + self.user_group_mgr.update_user_group_by_vo( + {"users": users}, user_group_vo=user_group_vo + ) + + # Delete projects + project_vos = self.project_mgr.filter_projects( + users=user_vo.user_id, domain_id=user_vo.domain_id + ) + for project_vo in project_vos: + users = project_vo.users + users.remove(user_vo.user_id) + self.project_mgr.update_project_by_vo( + {"users": users}, project_vo=project_vo + ) + + # Delete workspace groups + workspace_group_vos = self.workspace_group_mgr.filter_workspace_groups( + users__user_id=user_vo.user_id, domain_id=user_vo.domain_id + ) + + for workspace_group_vo in workspace_group_vos: + workspace_group_dict = workspace_group_vo.to_mongo().to_dict() + users = workspace_group_dict.get("users", []) + + if users: + updated_users = [ + user for user in users if user.get("user_id") != user_vo.user_id + ] + + if len(updated_users) != len(users): + self.workspace_group_mgr.update_workspace_group_by_vo( + {"users": updated_users}, workspace_group_vo=workspace_group_vo + ) + + self.user_mgr.delete_user(user_vo) + + def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: + user_rb_ids = self.role_binding_manager.stat_role_bindings( + query={ + "distinct": "user_id", + "filter": [ + {"k": "workspace_id", "v": workspace_id, "o": "eq"}, + {"k": "domain_id", "v": domain_id, "o": "eq"}, + ], + } + ).get("results", []) + return len(user_rb_ids) + + def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) + + if workspace_vo and workspace_vo.workspace_id != "*": + user_rb_total_count = self._get_workspace_user_count( + workspace_id, domain_id + ) + self.workspace_mgr.update_workspace_by_vo( + {"user_count": user_rb_total_count}, workspace_vo + ) diff --git a/src/spaceone/identity/service/user_service.py b/src/spaceone/identity/service/user_service.py index a8682810..b5477488 100644 --- a/src/spaceone/identity/service/user_service.py +++ b/src/spaceone/identity/service/user_service.py @@ -16,10 +16,12 @@ from spaceone.identity.manager.domain_secret_manager import DomainSecretManager from spaceone.identity.manager.email_manager import EmailManager from spaceone.identity.manager.external_auth_manager import ExternalAuthManager -from spaceone.identity.manager.token_manager.local_token_manager import ( - LocalTokenManager, -) +from spaceone.identity.manager.project_manager import ProjectManager +from spaceone.identity.manager.role_binding_manager import RoleBindingManager +from spaceone.identity.manager.user_group_manager import UserGroupManager from spaceone.identity.manager.user_manager import UserManager +from spaceone.identity.manager.workspace_group_manager import WorkspaceGroupManager +from spaceone.identity.manager.workspace_manager import WorkspaceManager from spaceone.identity.model.user.database import User from spaceone.identity.model.user.request import * from spaceone.identity.model.user.response import * @@ -39,6 +41,11 @@ def __init__(self, *args, **kwargs): self.user_mgr = UserManager() self.domain_mgr = DomainManager() self.domain_secret_mgr = DomainSecretManager() + self.rb_mgr = RoleBindingManager() + self.user_group_mgr = UserGroupManager() + self.project_mgr = ProjectManager() + self.workspace_mgr = WorkspaceManager() + self.workspace_group_mgr = WorkspaceGroupManager() @transaction(permission="identity:User.write", role_types=["DOMAIN_ADMIN"]) @convert_model @@ -418,7 +425,7 @@ def delete(self, params: UserDeleteRequest) -> None: if user_vo.role_type == "DOMAIN_ADMIN" and user_vo.state == "ENABLED": self._check_last_admin_user(params.domain_id, user_vo) - self.user_mgr.delete_user_by_vo(user_vo) + self.delete_user_by_vo(user_vo) @transaction(permission="identity:User.write", role_types=["DOMAIN_ADMIN"]) @convert_model @@ -912,3 +919,78 @@ def _should_reset_current_mfa( and enforce_mfa_type != user_mfa_type and user_mfa_type is not None ) + + def delete_user_by_vo(self, user_vo: User) -> None: + # Delete role bindings + rb_vos = self.rb_mgr.filter_role_bindings( + user_id=user_vo.user_id, domain_id=user_vo.domain_id + ) + for rb_vo in rb_vos: + self.rb_mgr.delete_role_binding_by_vo(rb_vo) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) + + # Delete user from user groups + user_group_vos = self.user_group_mgr.filter_user_groups( + users=user_vo.user_id, domain_id=user_vo.domain_id + ) + for user_group_vo in user_group_vos: + users = user_group_vo.users + users.remove(user_vo.user_id) + self.user_group_mgr.update_user_group_by_vo( + {"users": users}, user_group_vo=user_group_vo + ) + + # Delete projects + project_vos = self.project_mgr.filter_projects( + users=user_vo.user_id, domain_id=user_vo.domain_id + ) + for project_vo in project_vos: + users = project_vo.users + users.remove(user_vo.user_id) + self.project_mgr.update_project_by_vo( + {"users": users}, project_vo=project_vo + ) + + # Delete workspace groups + workspace_group_vos = self.workspace_group_mgr.filter_workspace_groups( + users__user_id=user_vo.user_id, domain_id=user_vo.domain_id + ) + + for workspace_group_vo in workspace_group_vos: + workspace_group_dict = workspace_group_vo.to_mongo().to_dict() + users = workspace_group_dict.get("users", []) + + if users: + updated_users = [ + user for user in users if user.get("user_id") != user_vo.user_id + ] + + if len(updated_users) != len(users): + self.workspace_group_mgr.update_workspace_group_by_vo( + {"users": updated_users}, workspace_group_vo=workspace_group_vo + ) + + self.user_mgr.delete_user(user_vo) + + def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: + user_rb_ids = self.rb_mgr.stat_role_bindings( + query={ + "distinct": "user_id", + "filter": [ + {"k": "workspace_id", "v": workspace_id, "o": "eq"}, + {"k": "domain_id", "v": domain_id, "o": "eq"}, + ], + } + ).get("results", []) + return len(user_rb_ids) + + def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) + + if workspace_vo and workspace_vo.workspace_id != "*": + user_rb_total_count = self._get_workspace_user_count( + workspace_id, domain_id + ) + self.workspace_mgr.update_workspace_by_vo( + {"user_count": user_rb_total_count}, workspace_vo + ) diff --git a/src/spaceone/identity/service/workspace_group_service.py b/src/spaceone/identity/service/workspace_group_service.py index 1932e362..8d414386 100644 --- a/src/spaceone/identity/service/workspace_group_service.py +++ b/src/spaceone/identity/service/workspace_group_service.py @@ -517,8 +517,10 @@ def delete_workspace_users_role_binding( workspace_id=workspace_group_workspace_ids, domain_id=domain_id, ) + for rb_vo in rb_vos: self.rb_mgr.delete_role_binding_by_vo(rb_vo) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) def add_users_to_workspace_group( self, @@ -665,6 +667,7 @@ def remove_users_from_workspace_group( if rb_vos.count() > 0: for rb_vo in rb_vos: self.rb_mgr.delete_role_binding_by_vo(rb_vo) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) updated_users = [user for user in old_users if user["user_id"] not in user_ids] @@ -743,3 +746,26 @@ def update_user_role_of_workspace_group( for role_binding_vo in role_binding_vos: role_binding_vo.update({"role_id": role_id, "role_type": role_type}) + + def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: + user_rb_ids = self.rb_mgr.stat_role_bindings( + query={ + "distinct": "user_id", + "filter": [ + {"k": "workspace_id", "v": workspace_id, "o": "eq"}, + {"k": "domain_id", "v": domain_id, "o": "eq"}, + ], + } + ).get("results", []) + return len(user_rb_ids) + + def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) + + if workspace_vo and workspace_vo.workspace_id != "*": + user_rb_total_count = self._get_workspace_user_count( + workspace_id, domain_id + ) + self.workspace_mgr.update_workspace_by_vo( + {"user_count": user_rb_total_count}, workspace_vo + ) diff --git a/src/spaceone/identity/service/workspace_service.py b/src/spaceone/identity/service/workspace_service.py index b77e3a51..1c6f711c 100644 --- a/src/spaceone/identity/service/workspace_service.py +++ b/src/spaceone/identity/service/workspace_service.py @@ -2,7 +2,6 @@ from datetime import datetime from typing import Dict, List, Union -from spaceone.core import cache from spaceone.core.service import * from spaceone.core.service.utils import * from spaceone.identity.error.error_workspace import * @@ -203,7 +202,7 @@ def delete(self, params: WorkspaceDeleteRequest) -> None: for rb_vo in rb_vos: self.rb_mgr.delete_role_binding_by_vo(rb_vo) - self.workspace_mgr.delete_workspace_by_vo(workspace_vo) + self.workspace_mgr.delete_workspace_by_vo(workspace_vo) @transaction(permission="identity:Workspace.write", role_types=["DOMAIN_ADMIN"]) @convert_model @@ -631,6 +630,7 @@ def _delete_role_bindings( ) for rb_vo in rb_vos: self.rb_mgr.delete_role_binding_by_vo(rb_vo) + self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) @staticmethod def _create_role_bindings( @@ -651,3 +651,26 @@ def _create_role_bindings( "workspace_id": workspace_id, } ) + + def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: + user_rb_ids = self.rb_mgr.stat_role_bindings( + query={ + "distinct": "user_id", + "filter": [ + {"k": "workspace_id", "v": workspace_id, "o": "eq"}, + {"k": "domain_id", "v": domain_id, "o": "eq"}, + ], + } + ).get("results", []) + return len(user_rb_ids) + + def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) + + if workspace_vo and workspace_vo.workspace_id != "*": + user_rb_total_count = self._get_workspace_user_count( + workspace_id, domain_id + ) + self.workspace_mgr.update_workspace_by_vo( + {"user_count": user_rb_total_count}, workspace_vo + ) From 0128bd9a5ba8776252f94a5c38a9674a1457732c Mon Sep 17 00:00:00 2001 From: daeyeon ko Date: Tue, 26 Aug 2025 18:38:10 +0900 Subject: [PATCH 3/4] feat: Prevents disabling roles in use by role bindings Signed-off-by: daeyeon ko --- src/spaceone/identity/service/role_service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/spaceone/identity/service/role_service.py b/src/spaceone/identity/service/role_service.py index 257bdd49..80d8d206 100644 --- a/src/spaceone/identity/service/role_service.py +++ b/src/spaceone/identity/service/role_service.py @@ -113,6 +113,14 @@ def disable(self, params: RoleDisableRequest) -> Union[RoleResponse, dict]: """ role_vo = self.role_mgr.get_role(params.role_id, params.domain_id) + + rb_vos = self.rb_mgr.filter_role_bindings( + role_id=role_vo.role_id, domain_id=role_vo.domain_id + ) + + if rb_vos.count() > 0: + raise ERROR_ROLE_IN_USED_AT_ROLE_BINDING(role_id=role_vo.role_id) + role_vo = self.role_mgr.disable_role_by_vo(role_vo) return RoleResponse(**role_vo.to_dict()) From 282ee54e006d84d0638c7a686396c88466f757ff Mon Sep 17 00:00:00 2001 From: daeyeon ko Date: Wed, 27 Aug 2025 16:22:43 +0900 Subject: [PATCH 4/4] refactor: update user count handling condition Signed-off-by: daeyeon ko --- src/spaceone/identity/service/domain_service.py | 6 ++++-- src/spaceone/identity/service/role_binding_service.py | 3 +++ src/spaceone/identity/service/system_service.py | 7 +++++-- src/spaceone/identity/service/user_service.py | 7 +++++-- src/spaceone/identity/service/workspace_group_service.py | 3 +++ src/spaceone/identity/service/workspace_service.py | 3 +++ 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/spaceone/identity/service/domain_service.py b/src/spaceone/identity/service/domain_service.py index 2d635e3c..53b8a548 100644 --- a/src/spaceone/identity/service/domain_service.py +++ b/src/spaceone/identity/service/domain_service.py @@ -90,8 +90,7 @@ def create(self, params: DomainCreateRequest) -> Union[DomainResponse, dict]: "role_type": role_vos[0].role_type, } - rb_vo = role_binding_mgr.create_role_binding(params_rb) - self._update_workspace_user_count(rb_vo.workspace_id, rb_vo.domain_id) + role_binding_mgr.create_role_binding(params_rb) return DomainResponse(**domain_vo.to_dict()) @@ -296,6 +295,9 @@ def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: return len(user_rb_ids) def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + if not workspace_id and not domain_id: + return + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) if workspace_vo and workspace_vo.workspace_id != "*": diff --git a/src/spaceone/identity/service/role_binding_service.py b/src/spaceone/identity/service/role_binding_service.py index 0e0deb98..53b14838 100644 --- a/src/spaceone/identity/service/role_binding_service.py +++ b/src/spaceone/identity/service/role_binding_service.py @@ -465,6 +465,9 @@ def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: return len(user_rb_ids) def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + if not workspace_id and not domain_id: + return + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) if workspace_vo and workspace_vo.workspace_id != "*": diff --git a/src/spaceone/identity/service/system_service.py b/src/spaceone/identity/service/system_service.py index a20d2b6b..c2f6a1b6 100644 --- a/src/spaceone/identity/service/system_service.py +++ b/src/spaceone/identity/service/system_service.py @@ -96,7 +96,7 @@ def init(self, params: SystemInitRequest) -> Union[SystemResponse, dict]: _LOGGER.debug( f"[init] Delete existing user in root domain: {user_vo.user_id}" ) - self.delete_user_by_vo(user_vo) + self._delete_user_by_vo(user_vo) # create admin user _LOGGER.debug(f"[init] Create admin user: {params.admin.user_id}") @@ -144,7 +144,7 @@ def init(self, params: SystemInitRequest) -> Union[SystemResponse, dict]: return SystemResponse(**response) - def delete_user_by_vo(self, user_vo: User) -> None: + def _delete_user_by_vo(self, user_vo: User) -> None: # Delete role bindings rb_vos = self.role_binding_manager.filter_role_bindings( user_id=user_vo.user_id, domain_id=user_vo.domain_id @@ -209,6 +209,9 @@ def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: return len(user_rb_ids) def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + if not workspace_id and not domain_id: + return + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) if workspace_vo and workspace_vo.workspace_id != "*": diff --git a/src/spaceone/identity/service/user_service.py b/src/spaceone/identity/service/user_service.py index b5477488..68bf9959 100644 --- a/src/spaceone/identity/service/user_service.py +++ b/src/spaceone/identity/service/user_service.py @@ -425,7 +425,7 @@ def delete(self, params: UserDeleteRequest) -> None: if user_vo.role_type == "DOMAIN_ADMIN" and user_vo.state == "ENABLED": self._check_last_admin_user(params.domain_id, user_vo) - self.delete_user_by_vo(user_vo) + self._delete_user_by_vo(user_vo) @transaction(permission="identity:User.write", role_types=["DOMAIN_ADMIN"]) @convert_model @@ -920,7 +920,7 @@ def _should_reset_current_mfa( and user_mfa_type is not None ) - def delete_user_by_vo(self, user_vo: User) -> None: + def _delete_user_by_vo(self, user_vo: User) -> None: # Delete role bindings rb_vos = self.rb_mgr.filter_role_bindings( user_id=user_vo.user_id, domain_id=user_vo.domain_id @@ -985,6 +985,9 @@ def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: return len(user_rb_ids) def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + if not workspace_id and not domain_id: + return + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) if workspace_vo and workspace_vo.workspace_id != "*": diff --git a/src/spaceone/identity/service/workspace_group_service.py b/src/spaceone/identity/service/workspace_group_service.py index 8d414386..775d6d26 100644 --- a/src/spaceone/identity/service/workspace_group_service.py +++ b/src/spaceone/identity/service/workspace_group_service.py @@ -760,6 +760,9 @@ def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: return len(user_rb_ids) def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + if not workspace_id and not domain_id: + return + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) if workspace_vo and workspace_vo.workspace_id != "*": diff --git a/src/spaceone/identity/service/workspace_service.py b/src/spaceone/identity/service/workspace_service.py index 1c6f711c..c2e22b17 100644 --- a/src/spaceone/identity/service/workspace_service.py +++ b/src/spaceone/identity/service/workspace_service.py @@ -665,6 +665,9 @@ def _get_workspace_user_count(self, workspace_id: str, domain_id: str) -> int: return len(user_rb_ids) def _update_workspace_user_count(self, workspace_id: str, domain_id: str) -> None: + if not workspace_id and not domain_id: + return + workspace_vo = self.workspace_mgr.get_workspace(workspace_id, domain_id) if workspace_vo and workspace_vo.workspace_id != "*":