Skip to content

Commit

Permalink
Merge pull request #1367 from Keeper-Security/DR-892_service_task_use…
Browse files Browse the repository at this point in the history
…r_management

DR-892 Add commands to control the user service/task mapping
  • Loading branch information
jwalstra-keeper authored Jan 29, 2025
2 parents 8f177f0 + 7647d2b commit cbdb93a
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 0 deletions.
18 changes: 18 additions & 0 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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')
Expand Down
17 changes: 17 additions & 0 deletions keepercommander/commands/pam_service/__init__.py
Original file line number Diff line number Diff line change
@@ -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()
160 changes: 160 additions & 0 deletions keepercommander/commands/pam_service/add.py
Original file line number Diff line number Diff line change
@@ -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}."
)
)
75 changes: 75 additions & 0 deletions keepercommander/commands/pam_service/list.py
Original file line number Diff line number Diff line change
@@ -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("")

104 changes: 104 additions & 0 deletions keepercommander/commands/pam_service/remove.py
Original file line number Diff line number Diff line change
@@ -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}."
)
)

0 comments on commit cbdb93a

Please sign in to comment.