You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

293 lines
15 KiB

#!/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.
#
clidir='/opt/sas/viya/home/bin/'
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='*'
# Import Python modules
import argparse
import subprocess
import json
import sys
from sharedfunctions import getfolderid,callrestapi
# 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 "%s: %s" % (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)