Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DR-892 Add commands to control the user service/task mapping #1367

Merged
merged 1 commit into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}."
)
)
Loading