From 7647d2b31a0b2edb13703b69f791b3b6619ba77d Mon Sep 17 00:00:00 2001 From: John Walstra Date: Tue, 28 Jan 2025 21:17:48 -0600 Subject: [PATCH] DR-892 Add commands to control the user service/task mapping Added the comamnds * pam action service list * pam action service add * pam action service remove These commands add graph mapping to indicate what machines a user is used for a serice or scheduled task. --- keepercommander/commands/discoveryrotation.py | 18 ++ .../commands/pam_service/__init__.py | 17 ++ keepercommander/commands/pam_service/add.py | 160 ++++++++++++++++++ keepercommander/commands/pam_service/list.py | 75 ++++++++ .../commands/pam_service/remove.py | 104 ++++++++++++ 5 files changed, 374 insertions(+) create mode 100644 keepercommander/commands/pam_service/__init__.py create mode 100644 keepercommander/commands/pam_service/add.py create mode 100644 keepercommander/commands/pam_service/list.py create mode 100644 keepercommander/commands/pam_service/remove.py diff --git a/keepercommander/commands/discoveryrotation.py b/keepercommander/commands/discoveryrotation.py index 8d0fba5f4..3c0ffe446 100644 --- a/keepercommander/commands/discoveryrotation.py +++ b/keepercommander/commands/discoveryrotation.py @@ -74,6 +74,9 @@ from .pam_debug.graph import PAMDebugGraphCommand from .pam_debug.info import PAMDebugInfoCommand from .pam_debug.gateway import PAMDebugGatewayCommand +from .pam_service.list import PAMActionServiceListCommand +from .pam_service.add import PAMActionServiceAddCommand +from .pam_service.remove import PAMActionServiceRemoveCommand def register_commands(commands): @@ -168,6 +171,19 @@ def __init__(self): self.default_verb = 'list' +class PAMActionServiceCommand(GroupCommand): + + def __init__(self): + super(PAMActionServiceCommand, self).__init__() + self.register_command('list', PAMActionServiceListCommand(), + 'List all mappings', 'l') + self.register_command('add', PAMActionServiceAddCommand(), + 'Add a user and machine to the mapping', 'a') + self.register_command('remove', PAMActionServiceRemoveCommand(), + 'Remove a user and machine from the mapping', 'r') + self.default_verb = 'list' + + class GatewayActionCommand(GroupCommand): def __init__(self): @@ -177,6 +193,8 @@ def __init__(self): self.register_command('rotate', PAMGatewayActionRotateCommand(), 'Rotate command', 'r') self.register_command('job-info', PAMGatewayActionJobCommand(), 'View Job details', 'ji') self.register_command('job-cancel', PAMGatewayActionJobCommand(), 'View Job details', 'jc') + self.register_command('service', PAMActionServiceCommand(), + 'Manage services and scheduled tasks user mappings.', 's') self.register_command('debug', PAMDebugCommand(), 'PAM debug information') # self.register_command('job-list', DRCmdListJobs(), 'List Running jobs') diff --git a/keepercommander/commands/pam_service/__init__.py b/keepercommander/commands/pam_service/__init__.py new file mode 100644 index 000000000..da7c1e345 --- /dev/null +++ b/keepercommander/commands/pam_service/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations +from ...utils import value_to_boolean +import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ...params import KeeperParams + from keepercommander.keeper_dag.connection import ConnectionBase + + +def get_connection(params: KeeperParams) -> ConnectionBase: + if value_to_boolean(os.environ.get("USE_LOCAL_DAG", False)) is False: + from keepercommander.keeper_dag.connection.commander import Connection as CommanderConnection + return CommanderConnection(params=params) + else: + from keepercommander.keeper_dag.connection.local import Connection as LocalConnection + return LocalConnection() \ No newline at end of file diff --git a/keepercommander/commands/pam_service/add.py b/keepercommander/commands/pam_service/add.py new file mode 100644 index 000000000..550a73e70 --- /dev/null +++ b/keepercommander/commands/pam_service/add.py @@ -0,0 +1,160 @@ +from __future__ import annotations +import argparse +from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext +from ...display import bcolors +from ... import vault +from keepercommander.discovery_common.user_service import UserService +from keepercommander.discovery_common.record_link import RecordLink +from keepercommander.discovery_common.constants import PAM_USER, PAM_MACHINE +from keepercommander.discovery_common.types import ServiceAcl +from keepercommander.keeper_dag.types import RefType, EdgeType +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from ...vault import TypedRecord + from ...params import KeeperParams + + +class PAMActionServiceAddCommand(PAMGatewayActionDiscoverCommandBase): + parser = argparse.ArgumentParser(prog='pam-action-service-add') + + # The record to base everything on. + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + + parser.add_argument('--machine-uid', '-m', required=True, dest='machine_uid', action='store', + help='The UID of the Windows Machine') + parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', + help='The UID of the User') + parser.add_argument('--type', '-t', required=True, choices=['service', 'task'], dest='type', + action='store', help='Relationship to add [service, task]') + + def get_parser(self): + return PAMActionServiceAddCommand.parser + + def execute(self, params: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + machine_uid = kwargs.get("machine_uid") + user_uid = kwargs.get("user_uid") + rel_type = kwargs.get("type") + + print("") + + gateway_context = GatewayContext.from_gateway(params, gateway) + if gateway_context is None: + print(f"{bcolors.FAIL}Could not find the gateway configuration for {gateway}.") + return + + if gateway_context is None: + print(f" {self._f('Cannot get gateway information. Gateway may not be up.')}") + return + + user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False) + record_link = RecordLink(record=gateway_context.configuration, params=params, fail_on_corrupt=False) + + ############### + + # Check to see if the record exists. + machine_record = vault.KeeperRecord.load(params, machine_uid) # type: Optional[TypedRecord] + if machine_record is None: + print(self._f("The machine record does not exists.")) + return + + # Make sure the record is a PAM Machine. + if machine_record.record_type != PAM_MACHINE: + print(self._f("The machine record is not a PAM Machine.")) + return + + # Make sure this machine is linked to the configuration record. + machine_rl = record_link.get_record_link(machine_record.record_uid) + if machine_rl.get_edge(record_link.dag.get_root, edge_type=EdgeType.LINK) is None: + print(self._f("The machine record does not belong to this gateway.")) + return + + ############### + + # Check to see if the record exists. + user_record = vault.KeeperRecord.load(params, user_uid) # type: Optional[TypedRecord] + if user_record is None: + print(self._f("The user record does not exists.")) + return + + # Make sure this user is a PAM User. + if user_record.record_type != PAM_USER: + print(self._f("The user record is not a PAM User.")) + return + + record_rotation = params.record_rotation_cache.get(user_record.record_uid) + if record_rotation is not None: + controller_uid = record_rotation.get("configuration_uid") + if controller_uid is None or controller_uid != gateway_context.configuration_uid: + print(self._f("The user record does not belong to this gateway. Cannot use this user.")) + return + else: + print(self._f("The user record does not have any rotation settings.")) + return + + ######## + + # Make sure we are setting up a Windows machine. + # Linux and Mac do not use passwords in services and cron jobs; no need to link. + os_field = next((x for x in machine_record.fields if x.label == "operatingSystem"), None) + if os_field is None: + print(self._f("Cannot find the operating system field in this record.")) + return + os_type = None + if len(os_field.value) > 0: + os_type = os_field.value[0] + if os_type is None: + print(self._f("The operating system field of the machine record is blank.")) + if os_type != "windows": + print(self._f("The operating system is not Windows. " + "PAM can only rotate the services and scheduled task password on Windows.")) + + # Get the machine service vertex. + # If it doesn't exist, create one. + machine_vertex = user_service.get_record_link(machine_record.record_uid) + if machine_vertex is None: + machine_vertex = user_service.dag.add_vertex( + uid=machine_record.record_uid, + name=machine_record.title, + vertex_type=RefType.PAM_MACHINE) + + # Get the user service vertex. + # If it doesn't exist, create one. + user_vertex = user_service.get_record_link(user_record.record_uid) + if user_vertex is None: + user_vertex = user_service.dag.add_vertex( + uid=user_record.record_uid, + name=user_record.title, + vertex_type=RefType.PAM_USER) + + # Get the existing service ACL and set the proper attribute. + acl = user_service.get_acl(machine_vertex.uid, user_vertex.uid) + if acl is None: + acl = ServiceAcl() + if rel_type == "service": + acl.is_service = True + else: + acl.is_task = True + + # Make sure the machine has a LINK connection to the configuration. + if user_service.dag.get_root.has(machine_vertex) is False: + user_service.belongs_to(gateway_context.configuration_uid, machine_vertex.uid) + + # Add our new ACL edge between the machine and the yser. + user_service.belongs_to(machine_vertex.uid, user_vertex.uid, acl=acl) + + text_type = "service" + if rel_type == "task": + text_type = "scheduled task" + + user_service.save() + + print( + self._gr( + f"{user_record.title} is connected to {machine_record.title}. " + f"The user, and password, is used on this machine for a {text_type}." + ) + ) diff --git a/keepercommander/commands/pam_service/list.py b/keepercommander/commands/pam_service/list.py new file mode 100644 index 000000000..39f3853de --- /dev/null +++ b/keepercommander/commands/pam_service/list.py @@ -0,0 +1,75 @@ +from __future__ import annotations +import argparse +from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext +from ...display import bcolors +from ... import vault +from keepercommander.discovery_common.user_service import UserService +from keepercommander.discovery_common.constants import PAM_MACHINE +from keepercommander.keeper_dag import EdgeType +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from ...vault import TypedRecord + from ...params import KeeperParams + + +class PAMActionServiceListCommand(PAMGatewayActionDiscoverCommandBase): + parser = argparse.ArgumentParser(prog='pam-action-service-list') + + # The record to base everything on. + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + + def get_parser(self): + return PAMActionServiceListCommand.parser + + def execute(self, params: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + + gateway_context = GatewayContext.from_gateway(params, gateway) + if gateway_context is None: + print(f"{bcolors.FAIL}Could not find the gateway configuration for {gateway}.") + return + + if gateway_context is None: + print(f" {self._f('Cannot get gateway information. Gateway may not be up.')}") + return + + user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False) + + service_map = {} + for resource_vertex in user_service.dag.get_root.has_vertices(edge_type=EdgeType.LINK): + resource_record = vault.KeeperRecord.load(params, resource_vertex.uid) # type: Optional[TypedRecord] + if resource_record is None or resource_record.record_type != PAM_MACHINE: + continue + user_vertices = user_service.get_user_vertices(resource_vertex.uid) + if len(user_vertices) > 0: + for user_vertex in user_vertices: + user_record = vault.KeeperRecord.load(params, user_vertex.uid) # type: Optional[TypedRecord] + if user_record is None: + continue + acl = user_service.get_acl(resource_record.record_uid, user_record.record_uid) + if acl is None or (acl.is_service is False and acl.is_task is False): + continue + if user_record.record_uid not in service_map: + service_map[user_record.record_uid] = { + "title": user_record.title, + "machines": [] + } + text = f"{resource_record.title} ({resource_record.record_uid}) :" + if acl.is_service is True: + text += f" {bcolors.OKGREEN}Services{bcolors.ENDC}" + if acl.is_task is True: + text += f" {bcolors.OKBLUE}Scheduled Tasks{bcolors.ENDC}" + service_map[user_record.record_uid]["machines"].append(text) + + print("") + print(self._h("User Mapping")) + for user_uid in service_map: + user = service_map[user_uid] + print(f" {self._b(user['title'])} ({user_uid})") + for machine in user["machines"]: + print(f" * {machine}") + print("") + diff --git a/keepercommander/commands/pam_service/remove.py b/keepercommander/commands/pam_service/remove.py new file mode 100644 index 000000000..5f68de9a0 --- /dev/null +++ b/keepercommander/commands/pam_service/remove.py @@ -0,0 +1,104 @@ +from __future__ import annotations +import argparse +from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext +from ...display import bcolors +from ... import vault +from keepercommander.discovery_common.constants import PAM_USER, PAM_MACHINE +from keepercommander.discovery_common.user_service import UserService +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from ...vault import TypedRecord + from ...params import KeeperParams + + +class PAMActionServiceRemoveCommand(PAMGatewayActionDiscoverCommandBase): + parser = argparse.ArgumentParser(prog='pam-action-service-remove') + + # The record to base everything on. + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + + parser.add_argument('--machine-uid', '-m', required=True, dest='machine_uid', action='store', + help='The UID of the Windows Machine') + parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', + help='The UID of the User') + parser.add_argument('--type', '-t', required=True, choices=['service', 'task'], dest='type', + action='store', help='Relationship to remove [service, task]') + + def get_parser(self): + return PAMActionServiceRemoveCommand.parser + + def execute(self, params: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + machine_uid = kwargs.get("machine_uid") + user_uid = kwargs.get("user_uid") + rel_type = kwargs.get("type") + + print("") + + gateway_context = GatewayContext.from_gateway(params, gateway) + if gateway_context is None: + print(f"{bcolors.FAIL}Could not find the gateway configuration for {gateway}.") + return + + if gateway_context is None: + print(f" {self._f('Cannot get gateway information. Gateway may not be up.')}") + return + + user_service = UserService(record=gateway_context.configuration, params=params, fail_on_corrupt=False) + + machine_record = vault.KeeperRecord.load(params, machine_uid) # type: Optional[TypedRecord] + if machine_record is None: + print(self._f("The machine record does not exists.")) + return + + if machine_record.record_type != PAM_MACHINE: + print(self._f("The machine record is not a PAM Machine.")) + return + + user_record = vault.KeeperRecord.load(params, user_uid) # type: Optional[TypedRecord] + if user_record is None: + print(self._f("The user record does not exists.")) + return + + if user_record.record_type != PAM_USER: + print(self._f("The user record is not a PAM User.")) + return + + machine_vertex = user_service.get_record_link(machine_record.record_uid) + if machine_vertex is None: + print(self._f(f"The machine {machine_record.title} does not exist in the mapping.")) + + user_vertex = user_service.get_record_link(user_record.record_uid) + if user_vertex is None: + print(self._f(f"The user {user_record.title} does not exist in the mapping.")) + + acl = user_service.get_acl(machine_vertex.uid, user_vertex.uid) + if acl is None: + print(f"{bcolors.WARNING}The user {user_record.title} did not control any services or " + f"scheduled tasks on {machine_record.title}{bcolors.ENDC}") + return + + if rel_type == "service": + acl.is_service = False + else: + acl.is_task = False + + if user_service.dag.get_root.has(machine_vertex) is False: + user_service.belongs_to(gateway_context.configuration_uid, machine_vertex.uid) + + user_service.belongs_to(machine_vertex.uid, user_vertex.uid, acl=acl) + + text_type = "services" + if rel_type == "task": + text_type = "scheduled tasks" + + user_service.save() + + print( + self._gr( + f"{user_record.title} was removed from {machine_record.title} for {text_type}." + ) + )