From 645f266b93afe3371486fb0a7a7ac0aba41dfa0f Mon Sep 17 00:00:00 2001 From: Rajendra Dendukuri Date: Tue, 7 Apr 2020 23:29:38 -0700 Subject: [PATCH] Create a shadow ZTP data json file accessible to non-root user To allow non-root user to view status information, a shadow file for the current ztp_data.json is created. The shadow file (ztp_data_shadow.json) contains only status information which does not provide knowledge of url's of the configuration scripts used for ztp. The ztp_data_shadow.json file is updated everytume ztp_data.json is updated. The ztp_data_shadow.json is accessible to non-root user as well while ztp_data.json is accessible only to root user and has more information about the url's where configuration scripts can be downloaded from. Signed-off-by: Rajendra Dendukuri --- src/usr/bin/ztp | 27 ++++++++++---- .../python3/dist-packages/ztp/ZTPSections.py | 37 +++++++++++++++++++ .../lib/python3/dist-packages/ztp/defaults.py | 1 + src/usr/lib/ztp/ztp-engine.py | 6 +++ tests/test_ztp_engine.py | 2 +- 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/usr/bin/ztp b/src/usr/bin/ztp index 3a9dc90..07e325c 100644 --- a/src/usr/bin/ztp +++ b/src/usr/bin/ztp @@ -119,6 +119,10 @@ def getActivityString(): print ('ZTP Service is not running\n') return + if os.geteuid() != 0: + print ('ZTP Service is active\n') + return + activity_str = None f = getCfg('ztp-activity') if os.path.isfile(f): @@ -160,6 +164,8 @@ def ztp_erase(yesFlag): # Destroy current provisioning data if os.path.isfile(getCfg('ztp-json', ztp_cfg=ztp_cfg)): os.remove(getCfg('ztp-json', ztp_cfg=ztp_cfg)) + if os.path.isfile(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg)): + os.remove(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg)) ## Administratively disable ZTP. # It also stops ztp service if it found active, before modifying the configuration. @@ -219,8 +225,8 @@ def ztp_run(yesFlag): def ztp_status_code(): if getCfg('admin-mode', ztp_cfg=ztp_cfg) is False: print ('0:DISABLED') - elif os.path.isfile(getCfg('ztp-json', ztp_cfg=ztp_cfg)): - objJson, jsonDict = JsonReader(getCfg('ztp-json', ztp_cfg=ztp_cfg), indent=4) + elif os.path.isfile(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg)): + objJson, jsonDict = JsonReader(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg), indent=4) ztpDict = jsonDict.get('ztp') if ztpDict.get('status') == 'BOOT': print ('3:NOT-STARTED') @@ -241,8 +247,8 @@ def ztp_status_code(): def ztp_status_terse(): # Print overall ZTP status print ('ZTP Admin Mode : %r' % getCfg('admin-mode', ztp_cfg=ztp_cfg)) - if os.path.isfile(getCfg('ztp-json', ztp_cfg=ztp_cfg)): - objJson, jsonDict = JsonReader(getCfg('ztp-json', ztp_cfg=ztp_cfg), indent=4) + if os.path.isfile(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg)): + objJson, jsonDict = JsonReader(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg), indent=4) ztpDict = jsonDict.get('ztp') if ztp_active() != 0: print ('ZTP Service : Inactive') @@ -284,8 +290,8 @@ def ztp_status(): print('%s' % 'ZTP') print('========================================') print ('ZTP Admin Mode : %r' % getCfg('admin-mode', ztp_cfg=ztp_cfg)) - if os.path.isfile(getCfg('ztp-json', ztp_cfg=ztp_cfg)): - objJson, jsonDict = JsonReader(getCfg('ztp-json', ztp_cfg=ztp_cfg), indent=4) + if os.path.isfile(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg)): + objJson, jsonDict = JsonReader(getCfg('ztp-json-shadow', ztp_cfg=ztp_cfg), indent=4) ztpDict = jsonDict.get('ztp') if ztp_active() != 0: print ('ZTP Service : Inactive') @@ -343,9 +349,11 @@ def ztp_status(): def main(): - # Only privileged users can execute this command + # Check the user's root privileges if os.geteuid() != 0: - sys.exit("Root privileges required for this operation") + root_user = False + else: + root_user = True # Add allowed arguments parser = argparse.ArgumentParser(description="Zero Touch Provisioning configuration and status monitoring tool", @@ -393,6 +401,9 @@ run Restart ZTP\nstatus Display current state of ZTP and last known resul print('Exiting ZTP service.') sys.exit(1) + if root_user == False and cmd in ['enable', 'disable', 'run', 'erase']: + sys.exit("Root privileges required for this operation") + # Execute the command try: if cmd == 'enable' : diff --git a/src/usr/lib/python3/dist-packages/ztp/ZTPSections.py b/src/usr/lib/python3/dist-packages/ztp/ZTPSections.py index 2060d8e..0da76d3 100644 --- a/src/usr/lib/python3/dist-packages/ztp/ZTPSections.py +++ b/src/usr/lib/python3/dist-packages/ztp/ZTPSections.py @@ -146,6 +146,39 @@ class ZTPJson(ConfigSection): '''! \brief This class is use to store and access ZTP JSON data downloaded by ZTP service. ''' + + def updateStatus(self, obj, status): + super().updateStatus(obj, status) + # Update the shadow ZTP JSON file with new information + self.__writeShadowJSON() + + def __writeShadowJSON(self): + '''! + Save contents of ZTP JSON in a shadow file which includes data that provides + just provisioning status information and filters out all other sensitive information. + ''' + allowed_keys = ['ignore-result', 'reboot-on-success', \ + 'reboot-on-failure', 'halt-on-failure', \ + 'description', 'timestamp', 'status', \ + 'start-timestamp', 'error'] + section_names = list() + shadowObj, shadowJsonDict = JsonReader(self.json_dst_file, getCfg('ztp-json-shadow'), indent=getCfg('json-indent')) + shadowDict = shadowJsonDict['ztp'] + for k, v in shadowDict.items(): + # Identify configuration sections + if isinstance(v, dict): + section_names.append(k) + + for section in section_names: + section_elements = list(shadowDict[section].keys()) + for sub_k in section_elements: + # Remove sensitive data + if sub_k not in allowed_keys: + del shadowDict[section][sub_k] + + shadowObj.writeJson() + os.chmod(getCfg('ztp-json-shadow'), 0o644) + def __getitem__(self, key): '''! Get value of specified key in the top level ztp section read from ZTP JSON file. @@ -175,6 +208,8 @@ def __setitem__(self, key, val): self.updateStatus(self.ztpDict, val) else: self.ztpDict[key] = val + # Update the shadow ZTP JSON file with new information + self.__writeShadowJSON() def pluginArgs(self, section_name): '''! @@ -420,3 +455,5 @@ def __init__(self, json_src_file=None, json_dst_file=None): # Write ZTP JSON data to file self.objJson.writeJson() + # Update the shadow ZTP JSON file with new information + self.__writeShadowJSON() diff --git a/src/usr/lib/python3/dist-packages/ztp/defaults.py b/src/usr/lib/python3/dist-packages/ztp/defaults.py index 0c0bb76..34464d3 100644 --- a/src/usr/lib/python3/dist-packages/ztp/defaults.py +++ b/src/usr/lib/python3/dist-packages/ztp/defaults.py @@ -62,6 +62,7 @@ "ztp-activity" : '/var/run/ztp/activity', \ "ztp-cfg-dir" : "/host/ztp", \ "ztp-json" : "/host/ztp/ztp_data.json", \ + "ztp-json-shadow" : "/host/ztp/ztp_data_shadow.json", \ "ztp-json-local" : "/host/ztp/ztp_data_local.json", \ "ztp-json-opt59" : "/var/run/ztp/ztp_data_opt59.json", \ "ztp-json-opt67" : "/var/run/ztp/ztp_data_opt67.json", \ diff --git a/src/usr/lib/ztp/ztp-engine.py b/src/usr/lib/ztp/ztp-engine.py index c85d862..25887d2 100755 --- a/src/usr/lib/ztp/ztp-engine.py +++ b/src/usr/lib/ztp/ztp-engine.py @@ -480,6 +480,8 @@ def __processZTPJson(self): logger.error('ZTP JSON file %s processing failed.' % (self.json_src)) try: os.remove(getCfg('ztp-json')) + if os.path.isfile(getCfg('ztp-json-shadow')): + os.remove(getCfg('ztp-json-shadow')) except OSError as v: if v.errno != errno.ENOENT: logger.warning('Exception [%s] encountered while deleting ZTP JSON file %s.' % (str(v), getCfg('ztp-json'))) @@ -507,6 +509,8 @@ def __processZTPJson(self): # Discover new ZTP data after deleting historic ZTP data logger.info("ZTP restart requested. Deleting previous ZTP session JSON data.") os.remove(getCfg('ztp-json')) + if os.path.isfile(getCfg('ztp-json-shadow')): + os.remove(getCfg('ztp-json-shadow')) self.objztpJson = None return ("retry", "ZTP restart requested") else: @@ -539,6 +543,8 @@ def __processZTPJson(self): # Mark ZTP for restart if _restart_ztp_missing_config or _restart_ztp_on_failure: os.remove(getCfg('ztp-json')) + if os.path.isfile(getCfg('ztp-json-shadow')): + os.remove(getCfg('ztp-json-shadow')) self.objztpJson = None # Remove startup-config file to obtain a new one through ZTP if getCfg('monitor-startup-config') is True and os.path.isfile(getCfg('config-db-json')): diff --git a/tests/test_ztp_engine.py b/tests/test_ztp_engine.py index b71d7d1..7d73d9c 100644 --- a/tests/test_ztp_engine.py +++ b/tests/test_ztp_engine.py @@ -129,7 +129,7 @@ def __init_ztp_data(self): runCommand("systemctl stop ztp") # Destroy current provisioning data file_list = ["ztp-json-local", "ztp-json-opt67", "ztp-json", "provisioning-script", "opt67-url", "opt59-v6-url", \ - "opt239-url", "opt239-v6-url", "ztp-restart-flag", "opt66-tftp-server", "acl-url", "graph-url"] + "opt239-url", "opt239-v6-url", "ztp-restart-flag", "opt66-tftp-server", "acl-url", "graph-url", "ztp-json-shadow"] for filename in file_list: if os.path.isfile(self.cfgGet(filename)):