From f945faf234ccf344e64396a51736923d52a4c5b7 Mon Sep 17 00:00:00 2001 From: sshrushanth-ks Date: Wed, 2 Sep 2026 12:34:16 +0530 Subject: [PATCH 1/4] Add multi-channel approver support to Google Chat App setup --- .../commands/integrations/gchat_app_setup.py | 32 +++++++++-- keepercommander/service/docker/models.py | 5 ++ .../service/test_docker_setup_ownership.py | 57 +++++++++++++++++++ unit-tests/service/test_gchat_app_setup.py | 25 ++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/keepercommander/service/commands/integrations/gchat_app_setup.py b/keepercommander/service/commands/integrations/gchat_app_setup.py index cdc1f00d4..8bf7aeac1 100644 --- a/keepercommander/service/commands/integrations/gchat_app_setup.py +++ b/keepercommander/service/commands/integrations/gchat_app_setup.py @@ -20,6 +20,11 @@ from .... import vault from ....display import bcolors from ...docker import GChatConfig, GChatConstants +from .approvals_setup import ( + ApprovalsChannelProfile, + approvals_config_to_record_fields, + print_approvals_config, +) from .integration_setup_base import IntegrationSetupCommand # Short Pub/Sub IDs, or full resource names: @@ -33,6 +38,19 @@ r'^projects/([^/]+)/topics/([A-Za-z][\w.-]{2,})$' ) +GCHAT_APPROVALS_PROFILE = ApprovalsChannelProfile( + channel_header='APPROVALS_SPACE_ID', + single_channel_description='Google Chat space ID for approval notifications', + channel_description='Google Chat space where approval requests for this approver team are sent', + default_channel_description=( + 'Default space for EPM privilege requests, SSO Cloud device approvals, ' + 'and approval requests from users not assigned to an approver team' + ), + channel_prompt='Space ID (starts with spaces/):', + validate_channel=lambda c: bool(c and c.startswith('spaces/')), + channel_error="Invalid Approvals Space ID (must start with 'spaces/')", +) + class GChatAppSetupCommand(IntegrationSetupCommand): """Google Chat App integration setup.""" @@ -52,6 +70,9 @@ def get_default_record_name(self) -> str: def get_integration_config_marker_field(self) -> str: return GChatConstants.FIELD_SERVICE_ACCOUNT_JSON + def get_approvals_profile(self): + return GCHAT_APPROVALS_PROFILE + # ── Google Chat-specific configuration ──────────────────────── def collect_integration_config(self, params): @@ -109,6 +130,10 @@ def collect_integration_config(self, params): GChatConstants.DEFAULT_COMMAND_CREATE_SECRET_ID, ) + profile = self.get_approvals_profile() + assert profile is not None + approvals = self._collect_approvals_config(params, profile) + pedm_enabled, pedm_interval = self._collect_pedm_config() da_enabled, da_interval = self._collect_device_approval_config() @@ -124,6 +149,7 @@ def collect_integration_config(self, params): chat_command_request_folder_id=chat_command_request_folder_id, chat_command_external_share_id=chat_command_external_share_id, chat_command_create_secret_id=chat_command_create_secret_id, + approvals=approvals, pedm_enabled=pedm_enabled, pedm_polling_interval=pedm_interval, device_approval_enabled=da_enabled, @@ -171,6 +197,7 @@ def build_record_custom_fields(self, config): config.chat_command_create_secret_id, GChatConstants.FIELD_COMMAND_CREATE_SECRET_ID, ), + *approvals_config_to_record_fields(config.approvals), vault.TypedField.new_field( 'text', 'true' if config.pedm_enabled else 'false', @@ -202,10 +229,6 @@ def print_integration_specific_resources(self, config): f" • Pub/Sub Subscription: " f"{bcolors.OKBLUE}{config.google_subscription_id}{bcolors.ENDC}" ) - print( - f" • Approvals Space: " - f"{bcolors.OKBLUE}{config.chat_approvals_space_id}{bcolors.ENDC}" - ) print( f" • /keeper-request-record ID: " f"{bcolors.OKBLUE}{config.chat_command_request_record_id}{bcolors.ENDC}" @@ -222,6 +245,7 @@ def print_integration_specific_resources(self, config): f" • /keeper-create-secret ID: " f"{bcolors.OKBLUE}{config.chat_command_create_secret_id}{bcolors.ENDC}" ) + print_approvals_config(config.approvals) def print_integration_commands(self): print(f"\n{bcolors.BOLD}Google Chat Commands Available:{bcolors.ENDC}") diff --git a/keepercommander/service/docker/models.py b/keepercommander/service/docker/models.py index c17950fe7..879cca190 100644 --- a/keepercommander/service/docker/models.py +++ b/keepercommander/service/docker/models.py @@ -155,6 +155,8 @@ class GChatConstants: FIELD_PEDM_POLLING_INTERVAL = 'pedm_polling_interval' FIELD_DEVICE_APPROVAL_ENABLED = 'device_approval_enabled' FIELD_DEVICE_APPROVAL_POLLING_INTERVAL = 'device_approval_polling_interval' + FIELD_MULTI_CHANNEL_ENABLED = 'multi_channel_approvers_enabled' + FIELD_APPROVALS_TEAMS = 'approvals_teams' DEFAULT_COMMAND_REQUEST_RECORD_ID = '1' DEFAULT_COMMAND_REQUEST_FOLDER_ID = '2' @@ -182,6 +184,9 @@ class GChatConfig: chat_command_request_folder_id: str = GChatConstants.DEFAULT_COMMAND_REQUEST_FOLDER_ID chat_command_external_share_id: str = GChatConstants.DEFAULT_COMMAND_EXTERNAL_SHARE_ID chat_command_create_secret_id: str = GChatConstants.DEFAULT_COMMAND_CREATE_SECRET_ID + approvals: ApprovalsConfig = field(default_factory=lambda: ApprovalsConfig( + multi_channel_enabled=False, single_channel_id='' + )) pedm_enabled: bool = False pedm_polling_interval: int = 120 device_approval_enabled: bool = False diff --git a/unit-tests/service/test_docker_setup_ownership.py b/unit-tests/service/test_docker_setup_ownership.py index 2dbf4cb02..e109d0726 100644 --- a/unit-tests/service/test_docker_setup_ownership.py +++ b/unit-tests/service/test_docker_setup_ownership.py @@ -312,6 +312,63 @@ def test_find_record_in_folder_skips_non_owned(self, mock_get): 'OWNED_REC', ) + def test_gchat_find_folder_uid_by_name_skips_non_owned(self): + from keepercommander.service.commands.integrations.gchat_app_setup import ( + GChatAppSetupCommand, + ) + + params = _params(shared_folder_cache={ + 'ATTACKER_SF': { + 'name': FOLDER_NAME, + 'owner_username': 'mallory@corp.example', + }, + 'OWNED_SF': { + 'name': FOLDER_NAME, + 'owner_username': 'operator@corp.example', + }, + }) + self.assertEqual( + GChatAppSetupCommand()._find_folder_uid_by_name(params, FOLDER_NAME), + 'OWNED_SF', + ) + + def test_gchat_find_folder_uid_by_name_returns_none_for_squat_only(self): + from keepercommander.service.commands.integrations.gchat_app_setup import ( + GChatAppSetupCommand, + ) + + params = _params(shared_folder_cache={ + 'ATTACKER_SF': { + 'name': FOLDER_NAME, + 'owner_username': 'mallory@corp.example', + }, + }) + self.assertIsNone( + GChatAppSetupCommand()._find_folder_uid_by_name(params, FOLDER_NAME) + ) + + @patch('keepercommander.service.commands.integrations.integration_setup_base.api.get_record') + def test_gchat_find_record_in_folder_skips_non_owned(self, mock_get): + from keepercommander.service.commands.integrations.gchat_app_setup import ( + GChatAppSetupCommand, + ) + + owned = MagicMock(title=RECORD_NAME) + shared = MagicMock(title=RECORD_NAME) + mock_get.side_effect = lambda _p, uid: shared if uid == 'SHARED_REC' else owned + + params = _params( + subfolder_record_cache={'FOLDER': ['SHARED_REC', 'OWNED_REC']}, + record_owner_cache={ + 'SHARED_REC': RecordOwner(False, 'attacker'), + 'OWNED_REC': RecordOwner(True, 'operator'), + }, + ) + self.assertEqual( + GChatAppSetupCommand()._find_record_in_folder(params, 'FOLDER', RECORD_NAME), + 'OWNED_REC', + ) + if __name__ == '__main__': unittest.main() diff --git a/unit-tests/service/test_gchat_app_setup.py b/unit-tests/service/test_gchat_app_setup.py index 5f44fbee3..2504c5c1e 100644 --- a/unit-tests/service/test_gchat_app_setup.py +++ b/unit-tests/service/test_gchat_app_setup.py @@ -191,6 +191,31 @@ def test_build_record_custom_fields(self): self.assertEqual(fields[GChatConstants.FIELD_PEDM_POLLING_INTERVAL], '60') self.assertEqual(fields[GChatConstants.FIELD_DEVICE_APPROVAL_ENABLED], 'false') + def test_get_approvals_profile(self): + profile = self.cmd.get_approvals_profile() + self.assertIsNotNone(profile) + self.assertEqual(profile.channel_header, 'APPROVALS_SPACE_ID') + self.assertEqual(profile.channel_prompt, 'Space ID (starts with spaces/):') + self.assertTrue(profile.validate_channel('spaces/AAAA')) + self.assertFalse(profile.validate_channel('invalid')) + + def test_build_record_custom_fields_includes_approvals_fields(self): + config = GChatConfig( + google_service_account_json='{"type":"service_account"}', + google_project_id='my-gcp-project', + google_subscription_id='keeper-chat-events', + google_topic_id='keeper-chat-topic', + chat_approvals_space_id='spaces/AAAA', + ) + fields = { + field.label: field.get_default_value() + for field in self.cmd.build_record_custom_fields(config) + } + self.assertIn(GChatConstants.FIELD_MULTI_CHANNEL_ENABLED, fields) + self.assertIn(GChatConstants.FIELD_APPROVALS_TEAMS, fields) + self.assertEqual(fields[GChatConstants.FIELD_MULTI_CHANNEL_ENABLED], 'false') + self.assertEqual(fields[GChatConstants.FIELD_APPROVALS_TEAMS], '') + if __name__ == '__main__': unittest.main() From efe3b88e13c3fa002b66d60084965c7a8ac5131a Mon Sep 17 00:00:00 2001 From: sshrushanth-ks Date: Wed, 2 Sep 2026 13:15:40 +0530 Subject: [PATCH 2/4] removed duplicate chat_approvals_space_id, and updates based on review comments --- .../commands/integrations/gchat_app_setup.py | 24 ++++-------- keepercommander/service/docker/models.py | 11 +++--- unit-tests/service/test_gchat_app_setup.py | 39 +++++++++++++------ 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/keepercommander/service/commands/integrations/gchat_app_setup.py b/keepercommander/service/commands/integrations/gchat_app_setup.py index 8bf7aeac1..de0f5587f 100644 --- a/keepercommander/service/commands/integrations/gchat_app_setup.py +++ b/keepercommander/service/commands/integrations/gchat_app_setup.py @@ -38,6 +38,11 @@ r'^projects/([^/]+)/topics/([A-Za-z][\w.-]{2,})$' ) +def _validate_gchat_space_id(space_id: str) -> bool: + prefix = GChatConstants.SPACE_ID_PREFIX + return bool(space_id and space_id.startswith(prefix) and len(space_id) > len(prefix)) + + GCHAT_APPROVALS_PROFILE = ApprovalsChannelProfile( channel_header='APPROVALS_SPACE_ID', single_channel_description='Google Chat space ID for approval notifications', @@ -47,8 +52,8 @@ 'and approval requests from users not assigned to an approver team' ), channel_prompt='Space ID (starts with spaces/):', - validate_channel=lambda c: bool(c and c.startswith('spaces/')), - channel_error="Invalid Approvals Space ID (must start with 'spaces/')", + validate_channel=_validate_gchat_space_id, + channel_error="Invalid Approvals Space ID (must start with 'spaces/' and include a space name)", ) @@ -103,14 +108,6 @@ def collect_integration_config(self, params): google_project_id, ) - print(f"\n{bcolors.BOLD}CHAT_APPROVALS_SPACE_ID:{bcolors.ENDC}") - print(f" Google Chat space where approval cards are posted") - chat_approvals_space_id = self._prompt_with_validation( - "Space ID (starts with spaces/):", - self._is_valid_space_id, - "Invalid Approvals Space ID (must start with 'spaces/' and include a space name)" - ) - print(f"\n{bcolors.BOLD}CHAT COMMAND IDs:{bcolors.ENDC}") print(f" Slash command IDs configured for the Google Chat app") chat_command_request_record_id = self._prompt_command_id( @@ -144,12 +141,11 @@ def collect_integration_config(self, params): google_project_id=google_project_id, google_subscription_id=google_subscription_id, google_topic_id=google_topic_id, - chat_approvals_space_id=chat_approvals_space_id, + approvals=approvals, chat_command_request_record_id=chat_command_request_record_id, chat_command_request_folder_id=chat_command_request_folder_id, chat_command_external_share_id=chat_command_external_share_id, chat_command_create_secret_id=chat_command_create_secret_id, - approvals=approvals, pedm_enabled=pedm_enabled, pedm_polling_interval=pedm_interval, device_approval_enabled=da_enabled, @@ -413,7 +409,3 @@ def _normalize_topic_id( google_project_id, ) - @staticmethod - def _is_valid_space_id(value: str) -> bool: - prefix = GChatConstants.SPACE_ID_PREFIX - return bool(value and value.startswith(prefix) and len(value) > len(prefix)) \ No newline at end of file diff --git a/keepercommander/service/docker/models.py b/keepercommander/service/docker/models.py index 879cca190..4c263602c 100644 --- a/keepercommander/service/docker/models.py +++ b/keepercommander/service/docker/models.py @@ -155,8 +155,6 @@ class GChatConstants: FIELD_PEDM_POLLING_INTERVAL = 'pedm_polling_interval' FIELD_DEVICE_APPROVAL_ENABLED = 'device_approval_enabled' FIELD_DEVICE_APPROVAL_POLLING_INTERVAL = 'device_approval_polling_interval' - FIELD_MULTI_CHANNEL_ENABLED = 'multi_channel_approvers_enabled' - FIELD_APPROVALS_TEAMS = 'approvals_teams' DEFAULT_COMMAND_REQUEST_RECORD_ID = '1' DEFAULT_COMMAND_REQUEST_FOLDER_ID = '2' @@ -179,15 +177,16 @@ class GChatConfig: google_project_id: str google_subscription_id: str google_topic_id: str - chat_approvals_space_id: str + approvals: ApprovalsConfig chat_command_request_record_id: str = GChatConstants.DEFAULT_COMMAND_REQUEST_RECORD_ID chat_command_request_folder_id: str = GChatConstants.DEFAULT_COMMAND_REQUEST_FOLDER_ID chat_command_external_share_id: str = GChatConstants.DEFAULT_COMMAND_EXTERNAL_SHARE_ID chat_command_create_secret_id: str = GChatConstants.DEFAULT_COMMAND_CREATE_SECRET_ID - approvals: ApprovalsConfig = field(default_factory=lambda: ApprovalsConfig( - multi_channel_enabled=False, single_channel_id='' - )) pedm_enabled: bool = False pedm_polling_interval: int = 120 device_approval_enabled: bool = False device_approval_polling_interval: int = 120 + + @property + def chat_approvals_space_id(self) -> str: + return self.approvals.single_channel_id diff --git a/unit-tests/service/test_gchat_app_setup.py b/unit-tests/service/test_gchat_app_setup.py index 2504c5c1e..54fe1c908 100644 --- a/unit-tests/service/test_gchat_app_setup.py +++ b/unit-tests/service/test_gchat_app_setup.py @@ -4,7 +4,12 @@ import unittest from keepercommander.service.commands.integrations.gchat_app_setup import GChatAppSetupCommand -from keepercommander.service.docker import GChatConfig, GChatConstants +from keepercommander.service.commands.integrations.approvals_setup import ( + FIELD_MULTI_CHANNEL_ENABLED, + FIELD_APPROVALS_CHANNEL_ID, + FIELD_APPROVALS_TEAMS, +) +from keepercommander.service.docker import GChatConfig, GChatConstants, ApprovalsConfig def _valid_service_account(**overrides): @@ -149,19 +154,26 @@ def test_normalize_topic_missing(self): self.assertIsNone(value) self.assertIn('required', error.lower()) - def test_space_id_validation(self): - self.assertTrue(self.cmd._is_valid_space_id('spaces/AAAA')) - self.assertFalse(self.cmd._is_valid_space_id('spaces/')) - self.assertFalse(self.cmd._is_valid_space_id('AAAA')) - self.assertFalse(self.cmd._is_valid_space_id('')) + def test_chat_approvals_space_id_property(self): + from keepercommander.service.docker import ApprovalsConfig + config = GChatConfig( + google_service_account_json='{"type":"service_account"}', + google_project_id='my-gcp-project', + google_subscription_id='keeper-chat-events', + google_topic_id='keeper-chat-topic', + approvals=ApprovalsConfig(multi_channel_enabled=False, single_channel_id='spaces/TEST'), + ) + self.assertEqual(config.chat_approvals_space_id, 'spaces/TEST') + self.assertEqual(config.chat_approvals_space_id, config.approvals.single_channel_id) def test_build_record_custom_fields(self): + from keepercommander.service.docker import ApprovalsConfig config = GChatConfig( google_service_account_json='{"type":"service_account"}', google_project_id='my-gcp-project', google_subscription_id='keeper-chat-events', google_topic_id='keeper-chat-topic', - chat_approvals_space_id='spaces/AAAA', + approvals=ApprovalsConfig(multi_channel_enabled=False, single_channel_id='spaces/AAAA'), chat_command_request_record_id='1', chat_command_request_folder_id='2', chat_command_external_share_id='3', @@ -200,21 +212,24 @@ def test_get_approvals_profile(self): self.assertFalse(profile.validate_channel('invalid')) def test_build_record_custom_fields_includes_approvals_fields(self): + from keepercommander.service.docker import ApprovalsConfig config = GChatConfig( google_service_account_json='{"type":"service_account"}', google_project_id='my-gcp-project', google_subscription_id='keeper-chat-events', google_topic_id='keeper-chat-topic', - chat_approvals_space_id='spaces/AAAA', + approvals=ApprovalsConfig(multi_channel_enabled=False, single_channel_id='spaces/AAAA'), ) fields = { field.label: field.get_default_value() for field in self.cmd.build_record_custom_fields(config) } - self.assertIn(GChatConstants.FIELD_MULTI_CHANNEL_ENABLED, fields) - self.assertIn(GChatConstants.FIELD_APPROVALS_TEAMS, fields) - self.assertEqual(fields[GChatConstants.FIELD_MULTI_CHANNEL_ENABLED], 'false') - self.assertEqual(fields[GChatConstants.FIELD_APPROVALS_TEAMS], '') + self.assertIn(FIELD_MULTI_CHANNEL_ENABLED, fields) + self.assertIn(FIELD_APPROVALS_CHANNEL_ID, fields) + self.assertIn(FIELD_APPROVALS_TEAMS, fields) + self.assertEqual(fields[FIELD_MULTI_CHANNEL_ENABLED], 'false') + self.assertEqual(fields[FIELD_APPROVALS_CHANNEL_ID], 'spaces/AAAA') + self.assertEqual(fields[FIELD_APPROVALS_TEAMS], '') if __name__ == '__main__': From 0cdf92d287bdcffb8cbf39ecc844ba14745d63b2 Mon Sep 17 00:00:00 2001 From: sshrushanth-ks Date: Wed, 2 Sep 2026 13:39:29 +0530 Subject: [PATCH 3/4] =?UTF-8?q?Fix=20review=20feedback=20on=20Google=20Cha?= =?UTF-8?q?t=20multi-channel=20approvers=20-Remove=20duplicate=20FIELD=5FA?= =?UTF-8?q?PPROVALS=5FSPACE=5FID=20from=20record=20fields=20-Fix=20type=20?= =?UTF-8?q?annotation:=20any=20=E2=86=92=20object=20in=20=5Fvalidate=5Fser?= =?UTF-8?q?vice=5Faccount=5Fdict()=20-Replace=20unsafe=20assert=20with=20p?= =?UTF-8?q?roper=20CommandError=20error=20handling=20-Add=20test=5Fmulti?= =?UTF-8?q?=5Fchannel=5Fapproval=5Fconfig=5Fserialization()=20for=20multi-?= =?UTF-8?q?team=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/integrations/gchat_app_setup.py | 14 +++++----- unit-tests/service/test_gchat_app_setup.py | 28 ++++++++++++++++++- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/keepercommander/service/commands/integrations/gchat_app_setup.py b/keepercommander/service/commands/integrations/gchat_app_setup.py index de0f5587f..469fbd62a 100644 --- a/keepercommander/service/commands/integrations/gchat_app_setup.py +++ b/keepercommander/service/commands/integrations/gchat_app_setup.py @@ -19,6 +19,7 @@ from .... import vault from ....display import bcolors +from ....error import CommandError from ...docker import GChatConfig, GChatConstants from .approvals_setup import ( ApprovalsChannelProfile, @@ -128,7 +129,11 @@ def collect_integration_config(self, params): ) profile = self.get_approvals_profile() - assert profile is not None + if profile is None: + raise CommandError( + self.get_command_name(), + 'Internal error: Google Chat approvals profile not configured' + ) approvals = self._collect_approvals_config(params, profile) pedm_enabled, pedm_interval = self._collect_pedm_config() @@ -168,11 +173,6 @@ def build_record_custom_fields(self, config): vault.TypedField.new_field( 'text', config.google_topic_id, GChatConstants.FIELD_TOPIC_ID ), - vault.TypedField.new_field( - 'text', - config.chat_approvals_space_id, - GChatConstants.FIELD_APPROVALS_SPACE_ID, - ), vault.TypedField.new_field( 'text', config.chat_command_request_record_id, @@ -335,7 +335,7 @@ def _load_service_account_json(cls, path: str) -> tuple[dict | None, str | None] return cls._validate_service_account_dict(data) @staticmethod - def _validate_service_account_dict(data: any) -> tuple[dict | None, str | None]: + def _validate_service_account_dict(data: object) -> tuple[dict | None, str | None]: if not isinstance(data, dict): return None, 'Service account JSON must be a JSON object' diff --git a/unit-tests/service/test_gchat_app_setup.py b/unit-tests/service/test_gchat_app_setup.py index 54fe1c908..7265af7f3 100644 --- a/unit-tests/service/test_gchat_app_setup.py +++ b/unit-tests/service/test_gchat_app_setup.py @@ -166,6 +166,32 @@ def test_chat_approvals_space_id_property(self): self.assertEqual(config.chat_approvals_space_id, 'spaces/TEST') self.assertEqual(config.chat_approvals_space_id, config.approvals.single_channel_id) + def test_multi_channel_approval_config_serialization(self): + from keepercommander.service.docker import ApprovalsConfig, ApproverTeam + from keepercommander.service.commands.integrations.approvals_setup import ( + approvals_config_to_record_fields, + ) + + config = ApprovalsConfig( + multi_channel_enabled=True, + single_channel_id='spaces/DEFAULT', + teams=[ + ApproverTeam(team_uid='team1', name='Team A', channel_id='spaces/A'), + ApproverTeam(team_uid='team2', name='Team B', channel_id='spaces/B', + folder_uids=['folder1']), + ] + ) + fields = { + field.label: field.get_default_value() + for field in approvals_config_to_record_fields(config) + } + self.assertEqual(fields[FIELD_MULTI_CHANNEL_ENABLED], 'true') + self.assertEqual(fields[FIELD_APPROVALS_CHANNEL_ID], 'spaces/DEFAULT') + teams_json = json.loads(fields[FIELD_APPROVALS_TEAMS]) + self.assertEqual(len(teams_json), 2) + self.assertEqual(teams_json[0]['team_uid'], 'team1') + self.assertEqual(teams_json[1]['folder_uids'], ['folder1']) + def test_build_record_custom_fields(self): from keepercommander.service.docker import ApprovalsConfig config = GChatConfig( @@ -194,7 +220,7 @@ def test_build_record_custom_fields(self): self.assertEqual(fields[GChatConstants.FIELD_PROJECT_ID], 'my-gcp-project') self.assertEqual(fields[GChatConstants.FIELD_SUBSCRIPTION_ID], 'keeper-chat-events') self.assertEqual(fields[GChatConstants.FIELD_TOPIC_ID], 'keeper-chat-topic') - self.assertEqual(fields[GChatConstants.FIELD_APPROVALS_SPACE_ID], 'spaces/AAAA') + self.assertEqual(fields[FIELD_APPROVALS_CHANNEL_ID], 'spaces/AAAA') self.assertEqual(fields[GChatConstants.FIELD_COMMAND_REQUEST_RECORD_ID], '1') self.assertEqual(fields[GChatConstants.FIELD_COMMAND_REQUEST_FOLDER_ID], '2') self.assertEqual(fields[GChatConstants.FIELD_COMMAND_EXTERNAL_SHARE_ID], '3') From 94dd3bc0d2047f86984d6625d1e6aa43b8c1992c Mon Sep 17 00:00:00 2001 From: sshrushanth-ks Date: Wed, 2 Sep 2026 15:31:03 +0530 Subject: [PATCH 4/4] Use GChat-specific naming for approvals configuration fields -Change default channel field to chat_approvals_space_id (from approvals_channel_id) -Use space_id instead of channel_id in approvals_teams JSON for teams -Create _gchat_approvals_record_fields() for GChat-specific serialization -Update tests to verify new field naming conventions --- .../commands/integrations/gchat_app_setup.py | 32 +++++++++++++++++-- unit-tests/service/test_gchat_app_setup.py | 31 +++++++----------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/keepercommander/service/commands/integrations/gchat_app_setup.py b/keepercommander/service/commands/integrations/gchat_app_setup.py index 469fbd62a..f216fbd8e 100644 --- a/keepercommander/service/commands/integrations/gchat_app_setup.py +++ b/keepercommander/service/commands/integrations/gchat_app_setup.py @@ -23,7 +23,6 @@ from ...docker import GChatConfig, GChatConstants from .approvals_setup import ( ApprovalsChannelProfile, - approvals_config_to_record_fields, print_approvals_config, ) from .integration_setup_base import IntegrationSetupCommand @@ -193,7 +192,7 @@ def build_record_custom_fields(self, config): config.chat_command_create_secret_id, GChatConstants.FIELD_COMMAND_CREATE_SECRET_ID, ), - *approvals_config_to_record_fields(config.approvals), + *self._gchat_approvals_record_fields(config.approvals), vault.TypedField.new_field( 'text', 'true' if config.pedm_enabled else 'false', @@ -216,6 +215,35 @@ def build_record_custom_fields(self, config): ), ] + def _gchat_approvals_record_fields(self, approvals): + import json + teams_json = '' + if approvals.multi_channel_enabled: + teams_data = [] + for team in approvals.teams: + teams_data.append({ + 'team_uid': team.team_uid, + 'name': team.name, + 'space_id': team.channel_id, + 'folder_uids': team.folder_uids, + 'record_uids': team.record_uids, + }) + teams_json = json.dumps(teams_data, indent=2) + + return [ + vault.TypedField.new_field( + 'text', + 'true' if approvals.multi_channel_enabled else 'false', + 'multi_channel_approvers_enabled', + ), + vault.TypedField.new_field( + 'text', + approvals.single_channel_id, + 'chat_approvals_space_id', + ), + vault.TypedField.new_field('multiline', teams_json, 'approvals_teams'), + ] + # ── Display ─────────────────────────────────────────────────── def print_integration_specific_resources(self, config): diff --git a/unit-tests/service/test_gchat_app_setup.py b/unit-tests/service/test_gchat_app_setup.py index 7265af7f3..e24fa5b4a 100644 --- a/unit-tests/service/test_gchat_app_setup.py +++ b/unit-tests/service/test_gchat_app_setup.py @@ -4,11 +4,6 @@ import unittest from keepercommander.service.commands.integrations.gchat_app_setup import GChatAppSetupCommand -from keepercommander.service.commands.integrations.approvals_setup import ( - FIELD_MULTI_CHANNEL_ENABLED, - FIELD_APPROVALS_CHANNEL_ID, - FIELD_APPROVALS_TEAMS, -) from keepercommander.service.docker import GChatConfig, GChatConstants, ApprovalsConfig @@ -168,9 +163,6 @@ def test_chat_approvals_space_id_property(self): def test_multi_channel_approval_config_serialization(self): from keepercommander.service.docker import ApprovalsConfig, ApproverTeam - from keepercommander.service.commands.integrations.approvals_setup import ( - approvals_config_to_record_fields, - ) config = ApprovalsConfig( multi_channel_enabled=True, @@ -183,13 +175,14 @@ def test_multi_channel_approval_config_serialization(self): ) fields = { field.label: field.get_default_value() - for field in approvals_config_to_record_fields(config) + for field in self.cmd._gchat_approvals_record_fields(config) } - self.assertEqual(fields[FIELD_MULTI_CHANNEL_ENABLED], 'true') - self.assertEqual(fields[FIELD_APPROVALS_CHANNEL_ID], 'spaces/DEFAULT') - teams_json = json.loads(fields[FIELD_APPROVALS_TEAMS]) + self.assertEqual(fields['multi_channel_approvers_enabled'], 'true') + self.assertEqual(fields['chat_approvals_space_id'], 'spaces/DEFAULT') + teams_json = json.loads(fields['approvals_teams']) self.assertEqual(len(teams_json), 2) self.assertEqual(teams_json[0]['team_uid'], 'team1') + self.assertEqual(teams_json[0]['space_id'], 'spaces/A') self.assertEqual(teams_json[1]['folder_uids'], ['folder1']) def test_build_record_custom_fields(self): @@ -220,7 +213,7 @@ def test_build_record_custom_fields(self): self.assertEqual(fields[GChatConstants.FIELD_PROJECT_ID], 'my-gcp-project') self.assertEqual(fields[GChatConstants.FIELD_SUBSCRIPTION_ID], 'keeper-chat-events') self.assertEqual(fields[GChatConstants.FIELD_TOPIC_ID], 'keeper-chat-topic') - self.assertEqual(fields[FIELD_APPROVALS_CHANNEL_ID], 'spaces/AAAA') + self.assertEqual(fields['chat_approvals_space_id'], 'spaces/AAAA') self.assertEqual(fields[GChatConstants.FIELD_COMMAND_REQUEST_RECORD_ID], '1') self.assertEqual(fields[GChatConstants.FIELD_COMMAND_REQUEST_FOLDER_ID], '2') self.assertEqual(fields[GChatConstants.FIELD_COMMAND_EXTERNAL_SHARE_ID], '3') @@ -250,12 +243,12 @@ def test_build_record_custom_fields_includes_approvals_fields(self): field.label: field.get_default_value() for field in self.cmd.build_record_custom_fields(config) } - self.assertIn(FIELD_MULTI_CHANNEL_ENABLED, fields) - self.assertIn(FIELD_APPROVALS_CHANNEL_ID, fields) - self.assertIn(FIELD_APPROVALS_TEAMS, fields) - self.assertEqual(fields[FIELD_MULTI_CHANNEL_ENABLED], 'false') - self.assertEqual(fields[FIELD_APPROVALS_CHANNEL_ID], 'spaces/AAAA') - self.assertEqual(fields[FIELD_APPROVALS_TEAMS], '') + self.assertIn('multi_channel_approvers_enabled', fields) + self.assertIn('chat_approvals_space_id', fields) + self.assertIn('approvals_teams', fields) + self.assertEqual(fields['multi_channel_approvers_enabled'], 'false') + self.assertEqual(fields['chat_approvals_space_id'], 'spaces/AAAA') + self.assertEqual(fields['approvals_teams'], '') if __name__ == '__main__':