text: #!/usr/bin/python
  # -*- coding: utf-8 -*-
  #
  # explainaccess.py
  # November 2018
  #
  # Usage:
  # explainaccess.py [-f folderpath | -u objectURI] [-n name_of_user_or_group -t user|group] [-p] [--header] [--direct_only] [-l permissions_list] [-c true|false] [-d]
  #
  # Examples:
  #
  # 1. Explain direct and indirect permissions on the folder /folderA/folderB, no header row. For folders, conveyed permissions are shown by default.
  #        ./explainaccess.py -f /folderA/folderB
  #
  # 2. As 1. but for a specific user named Heather
  #        ./explainaccess.py -f /folderA/folderB -n Heather -t user
  #
  # 3. As 1. with a header row
  #        ./explainaccess.py -f /folderA/folderB --header
  #
  # 4. As 1. with a header row and the folder path, which is useful if you concatenate sets of results in one file
  #        ./explainaccess.py -f /folderA/folderB -p --header
  #
  # 5. As 1. showing only rows which include a direct grant or prohibit
  #        ./explainaccess.py -f /folderA/folderB --direct_only
  #
  # 6. Explain direct and indirect permissions on a service endpoint. Note in the results that there are no conveyed permissions.
  #    By default they are not shown for URIs.
  #        ./explainaccess.py -u /SASEnvironmentManager/dashboard
  #
  # 7. As 6. but including a header row and the create permission, which is relevant for services but not for folders and other objects
  #        ./explainaccess.py -u /SASEnvironmentManager/dashboard --header -l read update delete secure add remove create
  #
  # 8. Explain direct and indirect permissions on a report, reducing the permissions reported to just read, update, delete and secure,
  #    since none of add, remove or create are applicable to a report.
  #        ./explainaccess.py -u /reports/reports/e2e0e601-b5a9-4601-829a-c5137f7441c6 --header -l read update delete secure
  #
  # 9. Explain direct and indirect permissions on a folder expressed as a URI. Keep the default permissions list, but for completeness
  #    we must also specify -c true to request conveyed permissions be displayed, as they are not displayed by default for URIs.
  #        ./explainaccess.py -u /folders/folders/9145d26a-2c0d-4523-8835-ad186bb57fa6 --header -p -c true
  #
  # Copyright © 2018, SAS Institute Inc., Cary, NC, USA.  All Rights Reserved.
  #
  #  Licensed under the Apache License, Version 2.0 (the License);
  #  you may not use this file except in compliance with the License.
  #  You may obtain a copy of the License at
  #
  #      http://www.apache.org/licenses/LICENSE-2.0
  #
  #  Unless required by applicable law or agreed to in writing, software
  #  distributed under the License is distributed on an "AS IS" BASIS,
  #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  #  See the License for the specific language governing permissions and
  #  limitations under the License.
  #
  
  # Import Python modules
  
  import argparse
  import subprocess
  import json
  import sys
  import os
  from sharedfunctions import getfolderid,callrestapi,getapplicationproperties
  
  # get python version
  version=int(str(sys.version_info[0]))
  
  # get cli location from properties
  propertylist=getapplicationproperties()
  
  clidir=propertylist["sascli.location"]
  cliexe=propertylist["sascli.executable"]
  
  clicommand=os.path.join(clidir,cliexe)
  
  debug=False
  direct_only=False
  valid_permissions=['read','update','delete','secure','add','remove','create']
  default_permissions=['read','update','delete','secure','add','remove']
  #direct_permission_suffix=u"\u2666" #Black diamond suit symbol - ok in stdout, seems to cause problems with other tools
  direct_permission_suffix='*'
  
  
  # Define exception handler so that we only output trace info from errors when in debug mode
  def exception_handler(exception_type, exception, traceback, debug_hook=sys.excepthook):
      if debug:
          debug_hook(exception_type, exception, traceback)
      else:
          print (exception_type.__name__, exception)
  
  sys.excepthook = exception_handler
  
  parser = argparse.ArgumentParser()
  parser.add_argument("-f","--folderpath", help="Path to a Viya folder. You must specify either -f folderpath or -u objectURI.")
  parser.add_argument("-u","--objecturi", help="Object URI. You must specify either -f folderpath or -u objectURI.")
  parser.add_argument("-n","--name", help="Enter the name of the user or group to test.")
  parser.add_argument("-t","--principaltype", help="Enter the type of principal to test: user or group.",choices=['user','group'])
  parser.add_argument("-p","--printpath", action='store_true', help="Print the folder path in each row")
  parser.add_argument("--header", action='store_true', help="Print a header row")
  parser.add_argument("--direct_only", action='store_true', help="Show only explanations which include a direct grant or prohibit")
  parser.add_argument("-l","--permissions_list", nargs="+", help="List of permissions, to include instead of all seven by default", default=default_permissions)
  parser.add_argument("-c","--convey", help="Show conveyed permissions in results. True by default when folder path is specified. False by dfefault if Object URI is specified.",choices=['true','false'])
  parser.add_argument("-d","--debug", action='store_true', help="Debug")
  args = parser.parse_args()
  path_to_folder=args.folderpath
  objecturi=args.objecturi
  name=args.name
  principaltype=args.principaltype
  printpath=args.printpath
  header=args.header
  direct_only=args.direct_only
  permissions=args.permissions_list
  conveyparam=args.convey
  debug=args.debug
  
  if path_to_folder and objecturi:
    raise Exception('You must specify either -f and a Viya folder path, or -u and an object URI, but not both.')
  if path_to_folder is None and objecturi is None:
    raise Exception('You must specify either -f and a Viya folder path, or -u and an object URI. You may not specify both.')
  
  if name and principaltype is None:
    raise Exception('If you specify a principal name, you must also specify a principal type which can be user or group.')
  if principaltype and name is None:
    raise Exception('If you specify a principal type, you must also specify a principal name.')
  
  for permission in permissions:
    if permission not in valid_permissions:
      raise Exception(permission+' is not the name of a permission. Valid permissions are: '+' '.join(map(str, valid_permissions)))
  
  # Two ways this program can be used: for a folder, or for a URI.
  if path_to_folder:
      getfolderid_result_json=getfolderid(path_to_folder)
  
      if (debug):
          print(getfolderid_result_json)
  
  
      if getfolderid_result_json[0] is not None:
          folder_uri=getfolderid_result_json[1]
          if (debug):
              print("Id  = "+getfolderid_result_json[0])
              print("URI = "+folder_uri)
              print("Path = "+getfolderid_result_json[2])
  
      explainuri=folder_uri
      resultpath=path_to_folder
      #Set convey to true, unless user overrode that setting and asked for false
      if(conveyparam is not None and conveyparam.lower()=='false'):
          convey=False
      else:
          convey=True
  
  else:
      explainuri=objecturi
      # This tool explains the permissions of any object.
      # If the object is a folder, we expect the user to supply path_to_folder, and we find its ID
      # If the object is something else, we don't have the path to the object.
      # It might be possible to get the path to the object from it's ID, but I'm not sure if there is a universal way to do that.
      # If the object is a report, you can call e.g.
      #    /opt/sas/viya/home/bin/sas-admin --output text reports show-info -id 43de1f98-d7ef-4490-bb46-cc177f995052
      # And the folder is one of the results passed back. But that call uses the reports plug-in to sas-admin and
      # should not be expected to return the path to other objects.
      # Plus, some objects do not have a path: service endpoints, for example.
      # This is a possible area for future improvement.
      resultpath=objecturi
      #Set convey to false, unless user overrode that setting and asked for true
      if(conveyparam is not None and conveyparam.lower()=='true'):
          convey=True
      else:
          convey=False
  
  
  #Use the /authorization/decision endpoint to ask for an explanation of the rules that are relevant to principals on this URI
  #See Authorization API documentation in swagger at http://swagger.na.sas.com/apis/authorization/v4/apidoc.html#op:createExplanation
  endpoint='/authorization/decision'
  if name and principaltype:
      if(principaltype.lower()=='user'):
          endpoint=endpoint+'?additionalUser='+name
      else:
          endpoint=endpoint+'?additionalGroup='+name
  method='post'
  accept='application/vnd.sas.authorization.explanations+json'
  content='application/vnd.sas.selection+json'
  inputdata={"resources":[explainuri]}
  
  decisions_result_json=callrestapi(endpoint,method,accept,content,inputdata)
  
  #print(decisions_result_json)
  #print('decisions_result_json is a '+type(decisions_result_json).__name__+' object') #decisions_result_json is a dict object
  e = decisions_result_json['explanations'][explainuri]
  
  #print('e is a '+type(e).__name__+' object') #e is a list object
  
  # Print header row if header argument was specified
  if header:
      if printpath:
          if convey:
              print('path,principal,'+','.join(map(str, permissions))+','+','.join(map('{0}(convey)'.format, permissions)))
          else:
              print('path,principal,'+','.join(map(str, permissions)))
      else:
          if convey:
              print('principal,'+','.join(map(str, permissions))+','+','.join(map('{0}(convey)'.format, permissions)))
          else:
              print('principal,'+','.join(map(str, permissions)))
  
  principal_found=False
  
  #For each principle's section in the explanations section of the data returned from the REST API call...
  for pi in e:
      #print pi['principal']
      #We are starting a new principal, so initialise some variables for this principal
      outstr=''
      has_a_direct_grant_or_deny=False
      if printpath:
          outstr=outstr+resultpath+','
      # If a name and principaltype are provided as arguments, we will only output a row for that principal
      if name and principaltype:
          if 'name' in pi['principal']:
              if (pi['principal']['name'].lower() == name.lower()):
                  principal_found=True
                  outstr=outstr+pi['principal']['name']
                  # Permissions on object
                  for permission in permissions:
                      # Not all objects have all the permissions
                      # Note that some objects do have permissions which are not meaningful for that object.
                      # E.g. SASAdministrators are granted Add and Remove on reports, by an OOTB rule which grants SASAdministrators all permissions (including Add and Remove) on /**.
                      # Meanwhile, Add and Remove are not shown in the View or Edit Authotizations dialogs for reports in EV etc.
                      # So, while it may be correct for the /authorization/decisions endpoint to explain that SASAdministrators are granted Add and Remove on a report,
                      # that does not alter the fact that in the context of a report, Add and Remove permissions are not meaningful.
                      if pi[permission.lower()]:
                          # This permission was in the expanation for this principal
                          outstr=outstr+','+pi[permission.lower()]['result']
                          if 'grantFactor' in pi[permission.lower()]:
                              if 'direct' in pi[permission.lower()]['grantFactor']:
                                  if pi[permission.lower()]['grantFactor']['direct']:
                                      has_a_direct_grant_or_deny=True
                                      outstr=outstr+direct_permission_suffix
                      else:
                          # This permission was absent from the expanation for this principal
                          outstr=outstr+','
                  # Conveyed permissions
                  if convey:
                      for permission in permissions:
                          # Only a few objects have conveyed permissions at all
                          if 'conveyedExplanation' in pi[permission.lower()]:
                              # This permission was in the expanation for this principal
                              outstr=outstr+','+pi[permission.lower()]['conveyedExplanation']['result']
                              if 'grantFactor' in pi[permission.lower()]['conveyedExplanation']:
                                  if 'direct' in pi[permission.lower()]['conveyedExplanation']['grantFactor']:
                                      if pi[permission.lower()]['conveyedExplanation']['grantFactor']['direct']:
                                          has_a_direct_grant_or_deny=True
                                          outstr=outstr+direct_permission_suffix
                          else:
                              # This permission was absent from the expanation for this principal
                              outstr=outstr+','
                  if direct_only:
                      if has_a_direct_grant_or_deny:
                          print(outstr)
                  else:
                      print(outstr)
      # But if no name or principaltype are provided, we output all rows
      else:
          if 'name' in pi['principal']:
              outstr=outstr+pi['principal']['name']
          else:
              outstr=outstr+pi['principal']['type']
          # Permissions on object
          for permission in permissions:
              # Not all objects have all the permissions
              if pi[permission.lower()]:
                  # This permission was in the expanation for this principal
                  outstr=outstr+','+pi[permission.lower()]['result']
                  if 'grantFactor' in pi[permission.lower()]:
                      if 'direct' in pi[permission.lower()]['grantFactor']:
                          if pi[permission.lower()]['grantFactor']['direct']:
                              has_a_direct_grant_or_deny=True
                              outstr=outstr+direct_permission_suffix
              else:
                  # This permission was absent from the expanation for this principal
                  outstr=outstr+','
          # Conveyed permissions
          if convey:
              for permission in permissions:
                  # Not all objects have all the permissions
                  if 'conveyedExplanation' in pi[permission.lower()]:
                      # This permission was in the expanation for this principal
                      outstr=outstr+','+pi[permission.lower()]['conveyedExplanation']['result']
                      if 'grantFactor' in pi[permission.lower()]['conveyedExplanation']:
                          if 'direct' in pi[permission.lower()]['conveyedExplanation']['grantFactor']:
                              if pi[permission.lower()]['conveyedExplanation']['grantFactor']['direct']:
                                  has_a_direct_grant_or_deny=True
                                  outstr=outstr+direct_permission_suffix
                  else:
                      # This permission was absent from the expanation for this principal
                      outstr=outstr+','
          if direct_only:
              if has_a_direct_grant_or_deny:
                  print(outstr)
          else:
              print(outstr)
  