Skip to content

Commit ca33b9b

Browse files
Refactor workflow code into files and folders
1 parent 58994bb commit ca33b9b

File tree

12 files changed

+1674
-1982
lines changed

12 files changed

+1674
-1982
lines changed

keepercommander/commands/discoveryrotation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
from .pam_debug.vertex import PAMDebugVertexCommand
7777
from .pam_import.commands import PAMProjectCommand
7878
from .pam_launch.launch import PAMLaunchCommand
79-
from .workflow.workflow_commands import PAMWorkflowCommand
79+
from .workflow import PAMWorkflowCommand
8080
from .pam_service.list import PAMActionServiceListCommand
8181
from .pam_service.add import PAMActionServiceAddCommand
8282
from .pam_service.remove import PAMActionServiceRemoveCommand

keepercommander/commands/pam_launch/launch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def execute(self, params: KeeperParams, **kwargs):
286286

287287
# Workflow access check and 2FA prompt
288288
try:
289-
from ..workflow.workflow_commands import check_workflow_and_prompt_2fa
289+
from ..workflow import check_workflow_and_prompt_2fa
290290
should_proceed, two_factor_value = check_workflow_and_prompt_2fa(params, record_uid)
291291
if not should_proceed:
292292
return

keepercommander/commands/tunnel_and_connections.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ def execute(self, params, **kwargs):
546546
# Workflow access check and 2FA prompt
547547
two_factor_value = None
548548
try:
549-
from .workflow.workflow_commands import check_workflow_and_prompt_2fa
549+
from .workflow import check_workflow_and_prompt_2fa
550550
should_proceed, two_factor_value = check_workflow_and_prompt_2fa(params, record_uid)
551551
if not should_proceed:
552552
return

keepercommander/commands/workflow/__init__.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,11 @@
55
# |_|
66
#
77
# Keeper Commander
8-
# Copyright 2024 Keeper Security Inc.
8+
# Copyright 2026 Keeper Security Inc.
99
# Contact: ops@keepersecurity.com
1010
#
1111

12-
"""
13-
Keeper PAM Workflow Commands
14-
15-
This module implements commands for managing PAM workflows including:
16-
- Configuration management (create, update, delete workflows)
17-
- Approver management (add, remove approvers)
18-
- Workflow state inspection (get status, list requests)
19-
- Workflow actions (request access, approve, deny, check-in/out)
20-
21-
Workflow commands are accessed via: pam workflow <subcommand>
22-
"""
23-
2412
__all__ = ['PAMWorkflowCommand', 'check_workflow_access', 'check_workflow_and_prompt_2fa']
2513

26-
from .workflow_commands import PAMWorkflowCommand, check_workflow_access, check_workflow_and_prompt_2fa
27-
14+
from .registry import PAMWorkflowCommand
15+
from .mfa import check_workflow_access, check_workflow_and_prompt_2fa
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# _ __
2+
# | |/ /___ ___ _ __ ___ _ _ ®
3+
# | ' </ -_) -_) '_ \/ -_) '_|
4+
# |_|\_\___\___| .__/\___|_|
5+
# |_|
6+
#
7+
# Keeper Commander
8+
# Copyright 2026 Keeper Security Inc.
9+
# Contact: ops@keepersecurity.com
10+
#
11+
12+
import argparse
13+
import json
14+
from datetime import datetime
15+
16+
from ..base import Command, dump_report_data
17+
from ..pam.router_helper import _post_request_to_router
18+
from ...display import bcolors
19+
from ...error import CommandError
20+
from ...params import KeeperParams
21+
from ...proto import workflow_pb2
22+
from ... import utils
23+
24+
from .helpers import RecordResolver, WorkflowFormatter
25+
26+
27+
class WorkflowGetApprovalRequestsCommand(Command):
28+
parser = argparse.ArgumentParser(
29+
prog='pam workflow pending',
30+
description='Get pending approval requests',
31+
)
32+
parser.add_argument('--format', dest='format', action='store',
33+
choices=['table', 'json'], default='table', help='Output format')
34+
35+
def get_parser(self):
36+
return WorkflowGetApprovalRequestsCommand.parser
37+
38+
def execute(self, params: KeeperParams, **kwargs):
39+
try:
40+
response = _post_request_to_router(
41+
params, 'get_approval_requests',
42+
rs_type=workflow_pb2.ApprovalRequests,
43+
)
44+
45+
if not response or not response.workflows:
46+
if kwargs.get('format') == 'json':
47+
print(json.dumps({'requests': []}, indent=2))
48+
else:
49+
print(f"\n{bcolors.WARNING}No approval requests{bcolors.ENDC}\n")
50+
return
51+
52+
wf_data = [
53+
(wf, self._resolve_status(params, wf))
54+
for wf in response.workflows
55+
]
56+
57+
if kwargs.get('format') == 'json':
58+
self._print_json(params, wf_data)
59+
else:
60+
self._print_table(params, wf_data)
61+
62+
except Exception as e:
63+
raise CommandError('', f'Failed to get approval requests: {str(e)}')
64+
65+
@staticmethod
66+
def _resolve_status(params, wf):
67+
if wf.startedOn:
68+
return 'Approved'
69+
try:
70+
st = workflow_pb2.WorkflowState()
71+
st.flowUid = wf.flowUid
72+
ws = _post_request_to_router(
73+
params, 'get_workflow_state',
74+
rq_proto=st, rs_type=workflow_pb2.WorkflowState,
75+
)
76+
if ws and ws.status and ws.status.stage in (
77+
workflow_pb2.WS_READY_TO_START, workflow_pb2.WS_STARTED,
78+
):
79+
return 'Approved'
80+
except Exception:
81+
pass
82+
return 'Pending'
83+
84+
@staticmethod
85+
def _print_json(params, wf_data):
86+
result = {
87+
'requests': [
88+
{
89+
'flow_uid': utils.base64_url_encode(wf.flowUid),
90+
'status': status,
91+
'requested_by': RecordResolver.resolve_user(params, wf.userId),
92+
'record_uid': utils.base64_url_encode(wf.resource.value),
93+
'record_name': RecordResolver.resolve_name(params, wf.resource),
94+
'started_on': wf.startedOn or None,
95+
'expires_on': wf.expiresOn or None,
96+
'duration': (
97+
WorkflowFormatter.format_duration(wf.expiresOn - wf.startedOn)
98+
if wf.expiresOn and wf.startedOn else None
99+
),
100+
'reason': wf.reason.decode('utf-8') if wf.reason else None,
101+
'external_ref': wf.externalRef.decode('utf-8') if wf.externalRef else None,
102+
}
103+
for wf, status in wf_data
104+
],
105+
}
106+
print(json.dumps(result, indent=2))
107+
108+
@staticmethod
109+
def _print_table(params, wf_data):
110+
rows = []
111+
for wf, status in wf_data:
112+
record_uid = utils.base64_url_encode(wf.resource.value) if wf.resource.value else ''
113+
record_name = RecordResolver.resolve_name(params, wf.resource)
114+
flow_uid = utils.base64_url_encode(wf.flowUid)
115+
requested_by = RecordResolver.resolve_user(params, wf.userId)
116+
started = (
117+
datetime.fromtimestamp(wf.startedOn / 1000).strftime('%Y-%m-%d %H:%M:%S')
118+
if wf.startedOn else ''
119+
)
120+
expires = (
121+
datetime.fromtimestamp(wf.expiresOn / 1000).strftime('%Y-%m-%d %H:%M:%S')
122+
if wf.expiresOn else ''
123+
)
124+
duration = (
125+
WorkflowFormatter.format_duration(wf.expiresOn - wf.startedOn)
126+
if wf.expiresOn and wf.startedOn else ''
127+
)
128+
rows.append([status, record_name, record_uid, flow_uid, requested_by, started, expires, duration])
129+
130+
headers = ['Status', 'Record Name', 'Record UID', 'Flow UID', 'Requested By', 'Started', 'Expires', 'Duration']
131+
print()
132+
dump_report_data(rows, headers=headers, sort_by=0)
133+
print()
134+
135+
136+
class WorkflowApproveCommand(Command):
137+
parser = argparse.ArgumentParser(
138+
prog='pam workflow approve',
139+
description='Approve a workflow access request',
140+
)
141+
parser.add_argument('flow_uid', help='Flow UID of the workflow to approve')
142+
parser.add_argument('--format', dest='format', action='store',
143+
choices=['table', 'json'], default='table', help='Output format')
144+
145+
def get_parser(self):
146+
return WorkflowApproveCommand.parser
147+
148+
def execute(self, params: KeeperParams, **kwargs):
149+
flow_uid = kwargs.get('flow_uid')
150+
flow_uid_bytes = utils.base64_url_decode(flow_uid)
151+
152+
approval = workflow_pb2.WorkflowApprovalOrDenial()
153+
approval.flowUid = flow_uid_bytes
154+
approval.deny = False
155+
156+
try:
157+
_post_request_to_router(params, 'approve_workflow_access', rq_proto=approval)
158+
159+
if kwargs.get('format') == 'json':
160+
result = {'status': 'success', 'flow_uid': flow_uid, 'action': 'approved'}
161+
print(json.dumps(result, indent=2))
162+
else:
163+
print(f"\n{bcolors.OKGREEN}✓ Access request approved{bcolors.ENDC}\n")
164+
print(f"Flow UID: {flow_uid}")
165+
print()
166+
167+
except Exception as e:
168+
raise CommandError('', f'Failed to approve request: {str(e)}')
169+
170+
171+
class WorkflowDenyCommand(Command):
172+
parser = argparse.ArgumentParser(
173+
prog='pam workflow deny',
174+
description='Deny a workflow access request',
175+
)
176+
parser.add_argument('flow_uid', help='Flow UID of the workflow to deny')
177+
parser.add_argument('-r', '--reason', help='Reason for denial')
178+
parser.add_argument('--format', dest='format', action='store',
179+
choices=['table', 'json'], default='table', help='Output format')
180+
181+
def get_parser(self):
182+
return WorkflowDenyCommand.parser
183+
184+
def execute(self, params: KeeperParams, **kwargs):
185+
flow_uid = kwargs.get('flow_uid')
186+
reason = kwargs.get('reason') or ''
187+
flow_uid_bytes = utils.base64_url_decode(flow_uid)
188+
189+
denial = workflow_pb2.WorkflowApprovalOrDenial()
190+
denial.flowUid = flow_uid_bytes
191+
denial.deny = True
192+
if reason:
193+
denial.denialReason = reason
194+
195+
try:
196+
_post_request_to_router(params, 'deny_workflow_access', rq_proto=denial)
197+
198+
if kwargs.get('format') == 'json':
199+
result = {'status': 'success', 'flow_uid': flow_uid, 'action': 'denied'}
200+
if reason:
201+
result['reason'] = reason
202+
print(json.dumps(result, indent=2))
203+
else:
204+
print(f"\n{bcolors.WARNING}Access request denied{bcolors.ENDC}\n")
205+
print(f"Flow UID: {flow_uid}")
206+
if reason:
207+
print(f"Reason: {reason}")
208+
print()
209+
210+
except Exception as e:
211+
raise CommandError('', f'Failed to deny request: {str(e)}')

0 commit comments

Comments
 (0)