Skip to content

Commit 21b27b4

Browse files
sshrushanth-ksmfordkeeper
authored andcommitted
KC-1377: Add --format json support for various Commands (#2265) (#2278)
* Add --format json support to whoami and PAM list/info commands * reverted changes for whoami and pam action job-list
1 parent 974b262 commit 21b27b4

9 files changed

Lines changed: 774 additions & 225 deletions

File tree

keepercommander/commands/discover/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,11 @@ def from_gateway(params: KeeperParams, gateway: str, configuration_uid: str | No
270270
if application is None:
271271
logging.debug(f"cannot find application for gateway {gateway}, skipping.")
272272

273-
if (utils.base64_url_encode(found_gateway.controllerUid) == gateway or
274-
found_gateway.controllerName.lower() == gateway.lower()):
273+
# When --configuration-uid selected the config, trust that selection.
274+
# Otherwise require the gateway name/UID to match.
275+
if (configuration_uid is not None
276+
or utils.base64_url_encode(found_gateway.controllerUid) == gateway
277+
or found_gateway.controllerName.lower() == gateway.lower()):
275278
return GatewayContext(
276279
configuration=configuration_record,
277280
facade=configuration_facade,

keepercommander/commands/discover/job_status.py

Lines changed: 189 additions & 86 deletions
Large diffs are not rendered by default.

keepercommander/commands/discover/rule_list.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
import argparse
3+
import json
34
from . import PAMGatewayActionDiscoverCommandBase, GatewayContext, MultiConfigurationException, multi_conf_msg
45
from ...display import bcolors
56
from ..pam.router_helper import router_get_connected_gateways
@@ -20,10 +21,29 @@ class PAMGatewayActionDiscoverRuleListCommand(PAMGatewayActionDiscoverCommandBas
2021

2122
parser.add_argument('--search', '-s', required=False, dest='search', action='store',
2223
help='Search for rules.')
24+
parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'],
25+
default='table', help='Output format (table, json)')
2326

2427
def get_parser(self):
2528
return PAMGatewayActionDiscoverRuleListCommand.parser
2629

30+
@staticmethod
31+
def _rule_to_dict(rule: RuleItem):
32+
action_value = None
33+
if getattr(rule, 'action', None) is not None:
34+
action_value = rule.action.value
35+
return {
36+
'rule_id': rule.rule_id,
37+
'name': rule.name or '',
38+
'action': action_value,
39+
'priority': rule.priority,
40+
'case_sensitive': bool(rule.case_sensitive),
41+
'added': rule.added_ts_str if rule.added_ts else '',
42+
'shared_folder_uid': getattr(rule, 'shared_folder_uid', None) or '',
43+
'admin_uid': getattr(rule, 'admin_uid', None) or '',
44+
'rule': Rules.make_action_rule_statement_str(rule.statement),
45+
}
46+
2747
@staticmethod
2848
def print_rule_table(rule_list: List[RuleItem]):
2949

@@ -90,21 +110,43 @@ def execute(self, params, **kwargs):
90110

91111
gateway = kwargs.get("gateway")
92112
configuration_uid = kwargs.get('configuration_uid')
113+
format_type = kwargs.get('format') or 'table'
93114
try:
94115
gateway_context = GatewayContext.from_gateway(params=params,
95116
gateway=gateway,
96117
configuration_uid=configuration_uid)
97118
if gateway_context is None:
98-
print(f"{bcolors.FAIL}Could not find the gateway configuration for {gateway}.{bcolors.ENDC}")
119+
message = f'Could not find the gateway configuration for {gateway}.'
120+
if format_type == 'json':
121+
print(json.dumps({'message': message}, indent=2))
122+
else:
123+
print(f"{bcolors.FAIL}{message}{bcolors.ENDC}")
99124
return
100125
except MultiConfigurationException as err:
101-
multi_conf_msg(gateway, err)
126+
if format_type == 'json':
127+
configs = []
128+
for item in (err.items or []):
129+
record = item.get('configuration_record')
130+
if record is not None:
131+
configs.append({
132+
'uid': getattr(record, 'record_uid', ''),
133+
'title': getattr(record, 'title', ''),
134+
})
135+
print(json.dumps({
136+
'message': f'Found multiple configuration records for gateway {gateway}.',
137+
'configurations': configs,
138+
}, indent=2))
139+
else:
140+
multi_conf_msg(gateway, err)
102141
return
103142

104143
rules = Rules(record=gateway_context.configuration, params=params)
105144
rule_list = rules.rule_list(rule_type=RuleTypeEnum.ACTION,
106145
search=kwargs.get("search")) # type: List[RuleItem]
107146
if len(rule_list) == 0:
147+
if format_type == 'json':
148+
print(json.dumps({'rules': []}, indent=2))
149+
return
108150
print("")
109151
text = f"{bcolors.FAIL}There are no rules. " \
110152
f"Use 'pam action discover rule add -g {gateway_context.gateway_uid} "
@@ -114,4 +156,13 @@ def execute(self, params, **kwargs):
114156
print(text)
115157
return
116158

159+
if format_type == 'json':
160+
print(json.dumps({
161+
'gateway': gateway_context.gateway_name,
162+
'gateway_uid': gateway_context.gateway_uid,
163+
'configuration_uid': gateway_context.configuration_uid,
164+
'rules': [self._rule_to_dict(rule) for rule in rule_list],
165+
}, indent=2))
166+
return
167+
117168
self.print_rule_table(rule_list=rule_list)

keepercommander/commands/discoveryrotation.py

Lines changed: 103 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,13 +1683,16 @@ class PAMListRecordRotationCommand(Command):
16831683
parser = argparse.ArgumentParser(prog='pam rotation list')
16841684
parser.add_argument('--verbose', '-v', required=False, default=False, dest='is_verbose', action='store_true',
16851685
help='Verbose output')
1686+
parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], default='table',
1687+
help='Output format (table, json)')
16861688

16871689
def get_parser(self):
16881690
return PAMListRecordRotationCommand.parser
16891691

16901692
def execute(self, params, **kwargs):
16911693

16921694
is_verbose = kwargs.get('is_verbose')
1695+
format_type = kwargs.get('format') or 'table'
16931696

16941697
rq = pam_pb2.PAMGenericUidsRequest()
16951698
schedules_proto = router_get_rotation_schedules(params, rq)
@@ -1708,19 +1711,27 @@ def execute(self, params, **kwargs):
17081711
all_pam_config_records = pam_configurations_get_all(params)
17091712
table = []
17101713

1711-
headers = []
1712-
headers.append('Record UID')
1713-
headers.append('Record Title')
1714-
headers.append('Record Type')
1715-
headers.append('Schedule')
1714+
if format_type == 'json':
1715+
headers = ['record_uid', 'record_title', 'record_type', 'schedule', 'gateway']
1716+
if is_verbose:
1717+
headers.append('gateway_uid')
1718+
headers.append('pam_configuration')
1719+
if is_verbose:
1720+
headers.append('pam_configuration_uid')
1721+
else:
1722+
headers = []
1723+
headers.append('Record UID')
1724+
headers.append('Record Title')
1725+
headers.append('Record Type')
1726+
headers.append('Schedule')
17161727

1717-
headers.append('Gateway')
1718-
if is_verbose:
1719-
headers.append('Gateway UID')
1728+
headers.append('Gateway')
1729+
if is_verbose:
1730+
headers.append('Gateway UID')
17201731

1721-
headers.append('PAM Configuration (Type)')
1722-
if is_verbose:
1723-
headers.append('PAM Configuration UID')
1732+
headers.append('PAM Configuration (Type)')
1733+
if is_verbose:
1734+
headers.append('PAM Configuration UID')
17241735

17251736
for s in schedules:
17261737
row = []
@@ -1737,22 +1748,27 @@ def execute(self, params, **kwargs):
17371748
is_controller_online = any(
17381749
(poc for poc in enterprise_controllers_connected_uids_bytes if poc == controller_uid))
17391750

1740-
row_color = ''
17411751
if record_exists_in_vault(params, record_uid):
1742-
row_color = bcolors.HIGHINTENSITYWHITE
1752+
record_accessible = True
17431753
record_title, record_type = get_vault_record_title_type(params, record_uid)
17441754
else:
1745-
row_color = bcolors.WHITE
1755+
record_accessible = False
17461756
record_title = '[record inaccessible]'
17471757
record_type = '[record inaccessible]'
17481758

17491759
if record_type != "pamUser":
17501760
# only pamUser records are supported for rotation
17511761
continue
17521762

1753-
row.append(f'{row_color}{record_uid}')
1754-
row.append(record_title or '[untitled]')
1755-
row.append(record_type or '[unknown]')
1763+
if format_type == 'json':
1764+
row.append(record_uid)
1765+
row.append(record_title or '[untitled]')
1766+
row.append(record_type or '[unknown]')
1767+
else:
1768+
row_color = bcolors.HIGHINTENSITYWHITE if record_accessible else bcolors.WHITE
1769+
row.append(f'{row_color}{record_uid}')
1770+
row.append(record_title or '[untitled]')
1771+
row.append(record_type or '[unknown]')
17561772

17571773
if s.noSchedule is True:
17581774
# Per Sergey A:
@@ -1769,9 +1785,15 @@ def execute(self, params, **kwargs):
17691785
else:
17701786
schedule_str = s.scheduleData
17711787
else:
1772-
schedule_str = f'{bcolors.FAIL}[empty]'
1788+
schedule_str = '[empty]'
17731789

1774-
row.append(f'{schedule_str}')
1790+
if format_type == 'json':
1791+
row.append(schedule_str)
1792+
else:
1793+
if schedule_str == '[empty]':
1794+
row.append(f'{bcolors.FAIL}[empty]')
1795+
else:
1796+
row.append(f'{schedule_str}')
17751797

17761798
# Controller Info
17771799
connected_controller = None
@@ -1780,29 +1802,45 @@ def execute(self, params, **kwargs):
17801802
list(enterprise_controllers_connected_resp.controllers)}
17811803
connected_controller = router_controllers.get(controller_details.controllerUid)
17821804

1783-
if connected_controller:
1784-
controller_stat_color = bcolors.OKGREEN
1805+
if format_type == 'json':
1806+
if controller_details:
1807+
row.append(controller_details.controllerName)
1808+
else:
1809+
row.append('[Does not exist]')
1810+
if is_verbose:
1811+
row.append(utils.base64_url_encode(controller_uid))
17851812
else:
1786-
controller_stat_color = bcolors.WHITE
1813+
if connected_controller:
1814+
controller_stat_color = bcolors.OKGREEN
1815+
else:
1816+
controller_stat_color = bcolors.WHITE
17871817

1788-
controller_color = bcolors.WHITE
1789-
if is_controller_online:
1790-
controller_color = bcolors.OKGREEN
1818+
controller_color = bcolors.WHITE
1819+
if is_controller_online:
1820+
controller_color = bcolors.OKGREEN
17911821

1792-
if controller_details:
1793-
row.append(f'{controller_stat_color}{controller_details.controllerName}{bcolors.ENDC}')
1794-
else:
1795-
row.append(f'{controller_stat_color}[Does not exist]{bcolors.ENDC}')
1822+
if controller_details:
1823+
row.append(f'{controller_stat_color}{controller_details.controllerName}{bcolors.ENDC}')
1824+
else:
1825+
row.append(f'{controller_stat_color}[Does not exist]{bcolors.ENDC}')
17961826

1797-
if is_verbose:
1798-
row.append(f'{controller_color}{utils.base64_url_encode(controller_uid)}{bcolors.ENDC}')
1827+
if is_verbose:
1828+
row.append(f'{controller_color}{utils.base64_url_encode(controller_uid)}{bcolors.ENDC}')
17991829

18001830
if not pam_configuration:
1801-
if not is_verbose:
1802-
row.append(f"{bcolors.FAIL}[No config found]{bcolors.ENDC}")
1831+
if format_type == 'json':
1832+
if not is_verbose:
1833+
row.append('[No config found]')
1834+
else:
1835+
row.append(
1836+
f'[No config found. Looks like configuration {configuration_uid_str} was removed '
1837+
f'but rotation schedule was not modified]')
18031838
else:
1804-
row.append(
1805-
f"{bcolors.FAIL}[No config found. Looks like configuration {configuration_uid_str} was removed but rotation schedule was not modified{bcolors.ENDC}")
1839+
if not is_verbose:
1840+
row.append(f"{bcolors.FAIL}[No config found]{bcolors.ENDC}")
1841+
else:
1842+
row.append(
1843+
f"{bcolors.FAIL}[No config found. Looks like configuration {configuration_uid_str} was removed but rotation schedule was not modified{bcolors.ENDC}")
18061844

18071845
else:
18081846
pam_config_name, pam_config_type = get_vault_record_title_type(params, configuration_uid_str)
@@ -1826,13 +1864,19 @@ def execute(self, params, **kwargs):
18261864
row.append(f"{pam_config_name or '[untitled]'} ({pam_config_type or '[unknown]'})")
18271865

18281866
if is_verbose:
1829-
row.append(f'{utils.base64_url_encode(configuration_uid)}{bcolors.ENDC}')
1867+
if format_type == 'json':
1868+
row.append(utils.base64_url_encode(configuration_uid))
1869+
else:
1870+
row.append(f'{utils.base64_url_encode(configuration_uid)}{bcolors.ENDC}')
18301871

18311872
table.append(row)
18321873

18331874
table.sort(key=lambda x: (x[1] or ''))
18341875

1835-
dump_report_data(table, headers, fmt='table', filename="", row_number=False, column_width=None)
1876+
report = dump_report_data(table, headers, fmt=format_type, filename="",
1877+
row_number=False, column_width=None)
1878+
if format_type == 'json':
1879+
return report
18361880

18371881
print(f"\n{bcolors.OKBLUE}----------------------------------------------------------{bcolors.ENDC}")
18381882
print(f"{bcolors.OKBLUE}Example to rotate record to which this user has access to:{bcolors.ENDC}")
@@ -4217,13 +4261,18 @@ class PAMGatewayActionServerInfoCommand(Command):
42174261
parser = argparse.ArgumentParser(prog='dr-info-command')
42184262
parser.add_argument('--gateway', '-g', required=False, dest='gateway_uid', action='store', help='Gateway UID')
42194263
parser.add_argument('--verbose', '-v', required=False, dest='verbose', action='store_true', help='Verbose Output')
4264+
parser.add_argument('--format', dest='format', action='store', choices=['text', 'json'],
4265+
default='text', help='Output format (text, json)')
42204266

42214267
def get_parser(self):
42224268
return PAMGatewayActionServerInfoCommand.parser
42234269

42244270
def execute(self, params, **kwargs):
4271+
from .pam.router_helper import get_response_payload
4272+
42254273
destination_gateway_uid_str = kwargs.get('gateway_uid')
42264274
is_verbose = kwargs.get('verbose')
4275+
format_type = kwargs.get('format') or 'text'
42274276
router_response = router_send_action_to_gateway(
42284277
params=params,
42294278
gateway_action=GatewayActionGatewayInfo(is_scheduled=False),
@@ -4232,6 +4281,23 @@ def execute(self, params, **kwargs):
42324281
destination_gateway_uid_str=destination_gateway_uid_str
42334282
)
42344283

4284+
if format_type == 'json':
4285+
if not router_response:
4286+
print(json.dumps({"gateway_info": None, "message": "No response from gateway."}, indent=2))
4287+
return
4288+
payload = get_response_payload(router_response)
4289+
if not (payload.get('is_ok') or payload.get('isOk')):
4290+
print(json.dumps({"ok": False, "response": payload}, indent=2))
4291+
return
4292+
result = {
4293+
"ok": True,
4294+
"gateway_info": payload.get('data'),
4295+
}
4296+
if payload.get('warnings'):
4297+
result['warnings'] = payload.get('warnings')
4298+
print(json.dumps(result, indent=2))
4299+
return
4300+
42354301
print_router_response(router_response, 'gateway_info', is_verbose=is_verbose,
42364302
gateway_uid=destination_gateway_uid_str)
42374303

0 commit comments

Comments
 (0)