diff --git a/ContributorAgreement.txt b/ContributorAgreement.txt new file mode 100644 index 0000000..8bbcef5 --- /dev/null +++ b/ContributorAgreement.txt @@ -0,0 +1,111 @@ +Contributor Agreement + + + +Version 1.1 + + + +Contributions to this software are accepted only when they are + +properly accompanied by a Contributor Agreement. The Contributor + +Agreement for this software is the Developer's Certificate of Origin + +1.1 (DCO) as provided with and required for accepting contributions + +to the Linux kernel. + + + +In each contribution proposed to be included in this software, the + +developer must include a "sign-off" that denotes consent to the + +terms of the Developer's Certificate of Origin. The sign-off is + +a line of text in the description that accompanies the change, + +certifying that you have the right to provide the contribution + +to be included. For changes provided in source code control (for + +example, via a Git pull request) the sign-off must be included in + +the commit message in source code control. For changes provided + +in email or issue tracking, the sign-off must be included in the + +email or the issue, and the sign-off will be incorporated into the + +permanent commit message if the contribution is accepted into the + +official source code. + + + +If you can certify the below: + + + + Developer's Certificate of Origin 1.1 + + + + By making a contribution to this project, I certify that: + + + + (a) The contribution was created in whole or in part by me and I + + have the right to submit it under the open source license + + indicated in the file; or + + + + (b) The contribution is based upon previous work that, to the best + + of my knowledge, is covered under an appropriate open source + + license and I have the right under that license to submit that + + work with modifications, whether created in whole or in part + + by me, under the same open source license (unless I am + + permitted to submit under a different license), as indicated + + in the file; or + + + + (c) The contribution was provided directly to me by some other + + person who certified (a), (b) or (c) and I have not modified + + it. + + + + (d) I understand and agree that this project and the contribution + + are public and that a record of the contribution (including all + + personal information I submit with it, including my sign-off) is + + maintained indefinitely and may be redistributed consistent with + + this project or the open source license(s) involved. + + + +then you just add a line saying + + + + Signed-off-by: Random J Developer + + + +using your real name (sorry, no pseudonyms or anonymous contributions.) \ No newline at end of file diff --git a/call_rest_api.py b/call_rest_api.py new file mode 100644 index 0000000..2091294 --- /dev/null +++ b/call_rest_api.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# call_rest_api.py +# +# Includes callrestapi.py, providing backward compatibility with previous version of this tool + +# +# 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 os +#print(os.path.dirname(os.path.realpath(__file__))) +scriptdir=os.path.dirname(os.path.realpath(__file__)) + +def include(filename): + if os.path.exists(filename): + execfile(filename) +include(os.path.join(scriptdir,'callrestapi.py')) + diff --git a/callrestapi.py b/callrestapi.py new file mode 100644 index 0000000..3c1bf5e --- /dev/null +++ b/callrestapi.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# callrestapi.py +# December 2017 +# +# Based on the items passed in the utility calls the rest api and return results +# +# Change History +# +# 27JAN2017 Comments added +# 29JAN2017 Added choices to validate method input +# 31JAN2017 Added contenttype parameters +# 02FEB2018 Added simple text print flag +# 01JUN2018 Renamed from call_rest_api.py to callrestapi.py +# 08JUN2018 Print json instead of pprint of easier result parsing +# 01JUN2018 Renamed from call_rest_api.py to callrestapi.py +# 08JUN2018 Print json instead of pprint of easier result parsing +# 08OCT2018 make printed json pretty +# 26OCT2018 call print function + + +# +# 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 argparse + +from sharedfunctions import callrestapi,getinputjson,printresult + +# get command line parameters + +parser = argparse.ArgumentParser(description="Call the Viya REST API") +parser.add_argument("-e","--endpoint", help="Enter the REST endpoint e.g. /folders/folders ",required='True') +parser.add_argument("-m","--method", help="Enter the REST method.",default="get",required='True',choices=['get','put','post','delete']) +parser.add_argument("-i","--inputfile",help="Enter the full path to an input json file",default=None) +parser.add_argument("-a","--accepttype",help="Enter REST Content Type you want returned e.g application/vnd.sas.identity.basic+json",default="application/json") +parser.add_argument("-c","--contenttype",help="Enter REST Content Type for POST e.g application/vnd.sas.identity.basic+json",default="application/json") +parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple'],default='json') +parser.add_argument("-t","--text", help="Display Simple Text Results.", action='store_true') + +args = parser.parse_args() + +reqval=args.endpoint +reqtype=args.method +reqfile=args.inputfile +reqcontent=args.contenttype +reqaccept=args.accepttype +simpletext=args.text +output_style=args.output + +# keep for backward compatibility +if simpletext: output_style='simple' + +# use the callrestapi function to make a call to the endpoint +# call passing json or not +if reqfile != None: + inputdata=getinputjson(reqfile) + result=callrestapi(reqval,reqtype,reqaccept,reqcontent,data=inputdata) +else: + result=callrestapi(reqval,reqtype,reqaccept,reqcontent) + +#print the result +printresult(result,output_style) diff --git a/createbinarybackup.py b/createbinarybackup.py new file mode 100644 index 0000000..6ef1eef --- /dev/null +++ b/createbinarybackup.py @@ -0,0 +1,131 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# createbinarybackup.py +# February 2018 +# +# Usage: +# python createbinarybackup.py [-q] [-d] +# +# +# 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. +# + +# CHANGE THIS VARIABLE IF YOUR CLI IS IN A DIFFERENT LOCATION +clidir='/opt/sas/viya/home/bin/' +debug=False +defaultBackupScheduleName="DEFAULT_BACKUP_SCHEDULE" +newScheduleName="BINARY_BACKUP_SCHEDULE" +newScheduleDesc="JobRequest to execute a binary backup" +jobDefinitionURIStem="/jobDefinitions/definitions/" +newScheduleContentType="application/vnd.sas.backup.request+json" # For a single-tenant deployment +#newScheduleContentType="application/vnd.sas.backup.deployment.request+json" # For a multi-tenant deployment + +# Import Python modules + +import argparse +import json +import sys + +from sharedfunctions import 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("-t","--principaltype", help="Enter the type of principal to test: user or group.",required='True',choices=['user','group']) +parser.add_argument("-q","--quiet", action='store_true') +parser.add_argument("-d","--debug", action='store_true') +args = parser.parse_args() +#principaltype=args.principaltype +quiet=args.quiet +debug=args.debug + +# STEP 1 of 4: Get the jobDefinition of the existing DEFAULT_BACKUP_SCHEDULE + +endpoint='/jobDefinitions/definitions?limit=20&filter=in(name,"'+defaultBackupScheduleName+'")' +method='get' +accept='application/json' + +jobDefinition_json=callrestapi(endpoint,method,accept) +if debug: + print('jobDefinition_json:') + print(jobDefinition_json) + +jobDefinitions=jobDefinition_json['items'] +id_found=False +jobDefinitionId='' +for jobDefinition in jobDefinitions: + if jobDefinition['name']: + if(jobDefinition['name']==defaultBackupScheduleName): + jobDefinitionId=jobDefinition['id'] + print('Id: '+jobDefinitionId) + id_found=True + +if not id_found: + raise Exception('Unable to determine Id for '+defaultBackupScheduleName+'.') + +# STEP 2 of 4: Create a jobExecution request + +endpoint='/jobExecution/jobRequests' +method='post' +accept='application/vnd.sas.job.execution.job.request+json' +content='application/vnd.sas.job.execution.job.request+json' +inputdata={ + "name": newScheduleName, + "description": newScheduleDesc, + "jobDefinitionUri": jobDefinitionURIStem+jobDefinitionId, + "arguments": { + "contentType": newScheduleContentType, + "backupType": "binary" + } +} + +jobExecutionRequest_json=callrestapi(endpoint,method,accept,content,inputdata) +if debug: + print('jobExecutionRequest_json:') + print(jobExecutionRequest_json) + +# STEP 3 of 4: Get the href to submit the job from the create jobExecution response + +links=jobExecutionRequest_json['links'] +href_found=False +submitJobHref='' +for link in links: + if link['rel']: + if(link['rel']=="submitJob"): + submitJobHref=link['href'] + print('Href: '+submitJobHref) + href_found=True + +if not href_found: + raise Exception('Unable to find the href for the submitJob link.') + +# STEP 4 of 4: Submit the jobExecution request + +endpoint=submitJobHref +method='post' +accept='application/vnd.sas.job.execution.job+json' + +submitJob_json=callrestapi(endpoint,method,accept) +#if debug: +print('submitJob_json:') +print(submitJob_json) diff --git a/createdomain.py b/createdomain.py new file mode 100644 index 0000000..f69b189 --- /dev/null +++ b/createdomain.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# createdomain.py +# December 2017 +# +# create a viya domain +# +# Change History +# +# 27JAN2017 Comments added +# 27JAN2017 Added the ability to create connection domains +# 29JAN2017 Added choices to validate type of domain +# 29SEP2018 make group list comma seperated +# + +# +# 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. +# + +# Create a domain + +import base64 +import argparse + +from sharedfunctions import callrestapi + +parser = argparse.ArgumentParser(description="Create a Viya Domain") +parser.add_argument("-d","--domain", help="Enter the domain name.",required=True) +parser.add_argument("-u","--user", help="User ID for the domain.",required=True) +parser.add_argument("-p","--password", help="Password for the userid.",required=False) +parser.add_argument("-g","--groups", help="A list of groups to add to the domain. Groupid comma seperated",required=True) +parser.add_argument("-c","--desc", help="Description of the domain.",required=False) +parser.add_argument("-t","--type", help="Type of the domain: password or connection (passwordless).",required=True, choices=['password','connection']) + +args = parser.parse_args() + +domain_name=args.domain +userid=args.user +pwval=args.password +groups=args.groups +desc=args.desc +type=args.type + +# create a python list with the groups +grouplist=groups.split(",") + +# encode the password +if pwval: + cred=base64.b64encode(pwval.encode("utf-8")).decode("utf-8") + +# build the rest call +reqval="/credentials/domains/"+domain_name +reqtype="put" + +# build the json parameters +data = {} +data['id'] = domain_name +data['description'] = desc +data['type'] = type + +# create the domain +callrestapi(reqval,reqtype,data=data) + +# for each group passed in add their credentials to the domain +for group_id in grouplist: + print("Adding "+ group_id + " to domain " + domain_name) + + reqval="/credentials/domains/"+domain_name+"/groups/"+group_id + reqtype="put" + + data = {} + data['domainId'] = domain_name + data['domainType'] = type + data['identityId'] = group_id + data['identityType'] = 'group' + data['properties']={"userId": userid} + if pwval: + data['secrets']={"password": cred} + + print(data) + + callrestapi(reqval,reqtype,data=data) + + diff --git a/createfolders.py b/createfolders.py new file mode 100644 index 0000000..3c7ee6c --- /dev/null +++ b/createfolders.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# createfolders.py +# October 2018 +# +# +# Change History +# +# sasgnn 30oct2018 Initial development +# +# Format of csv file is two columns +# Column 1 is the full path to the folder +# Column 2 is a description +# +# For example: +#/RnD, Folder under root for R&D +#/RnD/reports, reports +#/RnD/analysis, analysis +#/RnD/data plans, data plans +#/temp,My temporary folder + +# +# 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 argparse +import csv +import os +from sharedfunctions import callrestapi, getfolderid, file_accessible + +# setup command-line arguements +parser = argparse.ArgumentParser(description="Create folders that are read from a csv file") +parser.add_argument("-f","--file", help="Full path to csv file containing folders, format of csv: 'folderpath,description ",required='True') +args = parser.parse_args() +file=args.file + +reqtype="post" + +check=file_accessible(file,'r') + +# file can be read +if check: + + with open(file, 'rt') as f: + + filecontents = csv.reader(f) + for row in filecontents: + + #print(row) + newfolder=row[0] + description=row[1] + + + if newfolder[0]!='/': newfolder="/"+newfolder + + folder=os.path.basename(os.path.normpath(newfolder)) + parent_folder=os.path.dirname(newfolder) + + data = {} + data['name'] = folder + data['description'] = description + + + print ("Creating folder "+newfolder ) + + if parent_folder=="/": reqval='/folders/folders' + else: # parent folder create a child + + parentinfo=getfolderid(parent_folder) + + if parentinfo != None: + + parenturi=parentinfo[1] + reqval='/folders/folders?parentFolderUri='+parenturi + + else: print("Parent folder not found") + + myresult=callrestapi(reqval,reqtype,data=data,stoponerror=0) +else: + print("ERROR: cannot read "+file) + \ No newline at end of file diff --git a/deletefolder.py b/deletefolder.py new file mode 100644 index 0000000..b66ffdd --- /dev/null +++ b/deletefolder.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# deletefolder.py +# December 2017 +# +# Pass in a folder path and delete the folder, its sub-folders +# +# Change History +# +# 27JAN2018 Comments added +# 03Feb2018 Added quiet mode +# 03Mar2018 Made prompt comparison case-insensitive + +# +# 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, sys + +from sharedfunctions import getfolderid, callrestapi + +# get python version +version=int(str(sys.version_info[0])) + +# get input parameters +parser = argparse.ArgumentParser(description="Delete a folder and its sub-folders") +parser.add_argument("-f","--folderpath", help="Enter the path to the viya folder.",required='True') +parser.add_argument("-q","--quiet", help="Suppress the are you sure prompt.", action='store_true') +args = parser.parse_args() + +print(args.folderpath) +path_to_folder=args.folderpath +quietmode=args.quiet + +# call getfolderid to get the folder id +targets=getfolderid(path_to_folder) + + +# if folder is found +if targets[0] is not None: + + uri=targets[1] + + # if the user passed in the quiet key do not prompt are you sure + if quietmode: + areyousure="Y" + else: + + # deal with python 2 v python 3 prompts + if version > 2: + areyousure=input("Are you sure you want to delete the folder and its contents? (Y)") + else: + areyousure=raw_input("Are you sure you want to delete the folder and its contents? (Y)") + + + # delete the folder recursively + if areyousure.upper() =='Y': + print("Deleting folder= "+ path_to_folder+" "+uri) + reqval=uri+"?recursive=true" + reqtype='delete' + callrestapi(reqval,reqtype) + print('Folder Deleted.') + else: + print("Good thing I asked!") diff --git a/deletefolderandcontent.py b/deletefolderandcontent.py new file mode 100644 index 0000000..f6d011b --- /dev/null +++ b/deletefolderandcontent.py @@ -0,0 +1,103 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# deletefolderandcontent.py +# february 2018 +# +# Pass in a folder path and delete the folder, its sub-folders and content +# +# Change History +# +# 27JAN2018 Comments added +# 03Feb2018 Added quiet mode +# based on delete folder, but in addition deletes reports. + +# +# 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, sys + +from sharedfunctions import getfolderid, callrestapi + +# get python version +version=int(str(sys.version_info[0])) + +# get input parameters +parser = argparse.ArgumentParser(description="Delete a folder and its sub-folders and contents") +parser.add_argument("-f","--folderpath", help="Enter the path to the viya folder.",required='True') +parser.add_argument("-q","--quiet", help="Suppress the are you sure prompt.", action='store_true') +args = parser.parse_args() + +print(args.folderpath) +path_to_folder=args.folderpath +quietmode=args.quiet + +# call getfolderid to get the folder id +targets=getfolderid(path_to_folder) + +# if the folder is found +if targets[0] is not None: + + uri=targets[1] + + # if quiet do not prompt + if quietmode: + areyousure="Y" + else: + + if version > 2: + areyousure=input("Are you sure you want to delete the folder and its contents? (Y)") + else: + areyousure=raw_input("Are you sure you want to delete the folder and its contents? (Y)") + + if areyousure.upper() == 'Y': + + #delete folder content, recursive call returns all children + reqval=uri+"/members?recursive=true" + reqtype='get' + allchildren=callrestapi(reqval,reqtype) + + # get all child items + if 'items' in allchildren: + + itemlist=allchildren['items'] + + for children in itemlist: + + #if it is a report + if children['contentType']=='report': + + linklist=children['links'] + + for linkval in linklist: + + #find the delete method and call it + if linkval['rel']=='deleteResource': + reqval=(linkval['uri']) + reqtype=(linkval['method']).lower() + callrestapi(reqval,reqtype) + + print("Deleting folder= "+ path_to_folder+" "+uri) + + reqval=uri+"?recursive=true" + reqtype='delete' + callrestapi(reqval,reqtype) + print('Folder Deleted.') + + else: + print("Good thing I asked!") diff --git a/explainaccess.py b/explainaccess.py new file mode 100644 index 0000000..08ab791 --- /dev/null +++ b/explainaccess.py @@ -0,0 +1,293 @@ +#!/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) diff --git a/getconfigurationproperties.py b/getconfigurationproperties.py new file mode 100644 index 0000000..fbd8770 --- /dev/null +++ b/getconfigurationproperties.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# getconfigurationproperties.py +# December 2017 +# +# pass in the coniguration definition and return the properties +# +# Change History +# +# 27JAN2017 Comments added +# +# +# 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 +from __future__ import print_function +import argparse +import pprint +pp = pprint.PrettyPrinter(indent=4) + +from sharedfunctions import callrestapi, printresult + + +parser = argparse.ArgumentParser(description="Return a set of configuration properties") +parser.add_argument("-c","--configuration", help="Enter the configuration definition.",required='True') +parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple'],default='json') + +args = parser.parse_args() +configurationdef=args.configuration +output_style=args.output + +reqval="/configuration/configurations?definitionName="+configurationdef + +configvalues=callrestapi(reqval,'get') + +printresult(configvalues,output_style) + diff --git a/getfolderid.py b/getfolderid.py new file mode 100644 index 0000000..ae0b696 --- /dev/null +++ b/getfolderid.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# getfolder.py +# December 2017 +# +# getfolderid is a wrapper which sets up the command line arguements and then calls the getfolderid function +# the function returns a folderid and uri when passed the path to the folder in Viya +# +# Change History +# +# 27JAN2017 Comments added +# +# +# 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 argparse + +from sharedfunctions import getfolderid + +# setup command-line arguements +parser = argparse.ArgumentParser() +parser.add_argument("-f","--folderpath", help="Enter the path to the viya folder.",required='True') +args = parser.parse_args() +path_to_folder=args.folderpath + +# call the get folderid function and pass it the entered path +targets=getfolderid(path_to_folder) + +#print results if any are returned +if targets[0] is not None: + print("Id = "+targets[0]) + print("URI = "+targets[1]) + print("Path = "+targets[2]) \ No newline at end of file diff --git a/getruleid.py b/getruleid.py new file mode 100644 index 0000000..a973fe2 --- /dev/null +++ b/getruleid.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# getruleid.py +# December 2017 +# +# getruleid pass in a uri and identity and return the rule id +# +# Change History +# +# 27JAN2017 Comments added +# 18JUN2018 Output JSON +# +# 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 argparse + +from sharedfunctions import callrestapi, printresult + +# setup command-line arguements +parser = argparse.ArgumentParser() + +parser.add_argument("-u","--objecturi", help="Enter the objecturi.",required='True') +parser.add_argument("-p","--principal", help="Enter the identity name or authenticatedUsers, everyone or guest") +parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple'],default='json') + +args = parser.parse_args() +objuri=args.objecturi +ident=args.principal +output_style=args.output + +if ident.lower()=='authenticatedusers': ident='authenticatedUsers' + +if ident=='guest' or ident=='everyone' or ident=='authenticatedUsers': + reqval= "/authorization/rules?filter=and(eq(principalType,'"+ident+"'),eq(objectUri,'"+objuri+"'))" +else: + reqval= "/authorization/rules?filter=and(eq(principal,'"+ident+"'),eq(objectUri,'"+objuri+"'))" + +reqtype='get' + +result=callrestapi(reqval,reqtype) + +#print("ruleid= "+result['items'][0]['id']) + +# print rest call results +printresult(result,output_style) diff --git a/listrules.py b/listrules.py new file mode 100644 index 0000000..1b28f15 --- /dev/null +++ b/listrules.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# listrules.py +# August 2018 +# +# listrulesforidentity +# +# Change History +# +# 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 argparse + +from sharedfunctions import callrestapi, printresult + +# setup command-line arguements +parser = argparse.ArgumentParser(description="List rules for a principal and/or an endpoint") + +parser.add_argument("-u","--uri", help="Enter a string that the objecturi contains.",default="none") +parser.add_argument("-p","--principal", help="Enter the identity name or authenticatedUsers, everyone or guest",default='none') +parser.add_argument("-o","--output", help="Output Style", choices=['csv','json','simple'],default='json') + +args = parser.parse_args() +objuri=args.uri +ident=args.principal +output_style=args.output + +# set the limit high so that all data is returned +limitval=10000 + +# build the request depending on what options were passed in +if ident.lower()=='authenticatedusers': ident='authenticatedUsers' + +if ident=='none' and objuri=='none': reqval= "/authorization/rules" +elif ident=='none' and objuri != 'none': reqval= "/authorization/rules?filter=contains(objectUri,'"+objuri+"')" +elif ident!='none' and objuri == 'none': + if ident=='guest' or ident=='everyone' or ident=='authenticatedUsers': + reqval= "/authorization/rules?filter=eq(principalType,'"+ident+"')" + else: + reqval= "/authorization/rules?filter=eq(principal,'"+ident+"')" +elif ident!='none' and objuri != 'none': + + if ident=='guest' or ident=='everyone' or ident=='authenticatedUsers': + reqval= "/authorization/rules?filter=and(eq(principalType,'"+ident+"'),contains(objectUri,'"+objuri+"'))" + else: + reqval= "/authorization/rules?filter=and(eq(principal,'"+ident+"'),contains(objectUri,'"+objuri+"'))" + +if ident=='none' and objuri=='none': reqval=reqval+'?limit='+str(limitval) +else: reqval=reqval+'&limit='+str(limitval) + +reqtype='get' + +#make the rest call +result=callrestapi(reqval,reqtype) + +#print the result +printresult(result,output_style) + diff --git a/loginviauthinfo.py b/loginviauthinfo.py new file mode 100644 index 0000000..a4439fa --- /dev/null +++ b/loginviauthinfo.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# IMPORTANT: calls the sas-admin cli change the variable below if your CLI is not +# installed in the default location +# +# usage python loginviauthinfo.py +# +# Change History +# +# +# 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. +# +from __future__ import print_function +import netrc +import subprocess +import platform +import os +import argparse + +# CHANGE THIS VARIABLE IF YOUR CLI IS IN A DIFFERENT LOCATION +clidir='/opt/sas/viya/home/bin/' +#clidir='c:\\admincli\\' + +# get input parameters +parser = argparse.ArgumentParser(description="Authinfo File") +parser.add_argument("-f","--file", help="Enter the path to the authinfo file.",default='.authinfo') +args = parser.parse_args() +authfile=args.file + +host=platform.node() + +# Read from the authinfo file in your home directory +fname=os.path.join(os.path.expanduser('~'),authfile) + +cur_profile=os.environ.get("SAS_CLI_PROFILE","Default") +print("Logging in with profile: ",cur_profile ) + +if os.path.isfile(fname): + + secrets = netrc.netrc(fname) + username, account, password = secrets.authenticators( host ) + command=clidir+'sas-admin --profile '+cur_profile+ ' auth login -u '+username+ ' -p '+password + subprocess.call(command, shell=True) + +else: + print('ERROR: '+fname+' does not exist') \ No newline at end of file diff --git a/movecontent.py b/movecontent.py new file mode 100644 index 0000000..4990faf --- /dev/null +++ b/movecontent.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# movecontent.py +# December 2017 +# +# Moves content from one folder to another +# +# Change History +# +# 27JAN2018 Comments added +# 03Feb2018 Added quiet mode +# 03Mar2018 Made prompt comparison case-insensitive +# +# +# 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, sys + +from sharedfunctions import getfolderid, callrestapi + +# get python version +version=int(str(sys.version_info[0])) + +parser = argparse.ArgumentParser(description="Move content from a source to a target folder") +parser.add_argument("-s","--sourcefolder", help="Enter the path to the source folder.",required='True') +parser.add_argument("-t","--targetfolder", help="Enter the path to the source folder.",required='True') +parser.add_argument("-q","--quiet", help="Suppress the are you sure prompt.", action='store_true') +args = parser.parse_args() + +source=args.sourcefolder +target=args.targetfolder + +quietmode=args.quiet + +sourceinfo=getfolderid(source) +targetinfo=getfolderid(target) + +if sourceinfo[0] is not None: + + id=sourceinfo[0] + + if quietmode: + areyousure="Y" + else: + + if version > 2: + areyousure=input("Are you sure you want to move content from "+source+" to "+target+"? (Y)") + else: + areyousure=raw_input("Are you sure you want to move content from "+source+" to "+target+"? (Y)") + + if areyousure.upper() == 'Y': + + # get all the content in folder + reqtype='get' + reqval='/folders/folders/'+id+"/members" + members=callrestapi(reqval,reqtype) + + # create a list of items + items=members["items"] + + for item in items: + + # delete from folder + + reqtype="delete" + reqval='/folders/folders/'+id+"/members/"+item["id"] + rc=callrestapi(reqval,reqtype) + + #build dictionary of item + + thisitem={"uri":item["uri"],"name":item["name"],"type":item["type"],"contentType":item["contentType"]} + + #add to new folder + + reqtype="post" + reqval="/folders/folders/"+targetinfo[0]+"/members" + rc=callrestapi(reqval,reqtype,data=thisitem) + + print("NOTE: content moved between folder "+source+" and "+target) + + + else: + print("Good thing I asked!") diff --git a/sharedfunctions.py b/sharedfunctions.py new file mode 100644 index 0000000..bdc4546 --- /dev/null +++ b/sharedfunctions.py @@ -0,0 +1,521 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# sharedfunctions.py +# December 2017 +# +# A set of shared functions used by the piviyatool which makes REST calls to supplement the VIYA CLI +# +# callrestapi is the core function it will accept a method endpoint and optionally a python dictionary as input +# getfolderid returns a folder id if it is passed the path to the viya folder +# getebaseuri returns the base url for the service from the default profile +# getauthtoken returns the authentication token created by the CLI call sas-admin auth login +# getinputjson converts the input json to a python dictionary +# +# Change History +# +# 27JAN2018 Comments added +# 29JAN2018 Added simpleresults function +# 31JAN2018 Added the ability to pass contenttype to call_rest_api (now callrestapi) +# 31JAN2018 Improved error handling of call_rest_api (now callrestapi) +# 31JAN2018 Deal with situation where json is not returned +# 31JAN2018 Fix a bug when neither json or text is returned +# 02FEB2018 Fix a bug when text is returned +# 12MAR2018 Made simple result print generic +# 20MAR2018 Added some comments +# 20MAR2018 Handle errors when profile and authentication token do not exist +# 20May2018 Fixed bug in authentication check +# 01jun2018 Deal with empty profile error +# 23oct2018 Added print result function +# 23oct2018 Added print csv +# 28oct2018 Added stop on error to be able to override stopping processing when an error occurs +# 20nov2018 Updated so that multiple profiles can be used + +# +# 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 +# +# https://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 +from __future__ import print_function +import requests +import sys +import json +import pprint +import os + +pp = pprint.PrettyPrinter(indent=4) + +# validate rest api is not used at this time +# not used + +def validaterestapi(baseurl, reqval, reqtype, data={}): + + global result + + print("The request is a "+reqtype+" request: ",baseurl+reqval) + + json_data=json.dumps(data, ensure_ascii=False) + + print("Data for Request:") + print(json_data) + + if (reqtype !="get" or reqtype !="post" or reqtype!="delete" or reqtype!="put"): + print("NOTE: Invalid method") + + return; + +# callrestapi +# this is the main function called many other programs and by the callrestapi program to make the REST calls +# change history +# 01dec2017 initial development +# 28oct2018 Added stop on error to be able to override stopping processing when an error occurs + +def callrestapi(reqval, reqtype, acceptType='application/json', contentType='application/json',data={},stoponerror=1): + + + # get the url from the default profile + baseurl=getbaseurl() + + # get the auth token + oaval=getauthtoken(baseurl) + + # build the authorization header + head= {'Content-type':contentType,'Accept':acceptType} + head.update({"Authorization" : oaval}) + + # maybe this can be removed + global result + + # sereliaze the data string for the request to json format + json_data=json.dumps(data, ensure_ascii=False) + + # call the rest api using the parameters passed in and the requests python library + + if reqtype=="get": + ret = requests.get(baseurl+reqval,headers=head,data=json_data) + elif reqtype=="post": + ret = requests.post(baseurl+reqval,headers=head,data=json_data) + elif reqtype=="delete": + ret = requests.delete(baseurl+reqval,headers=head,data=json_data) + elif reqtype=="put": + ret = requests.put(baseurl+reqval,headers=head,data=json_data) + else: + result=None + print("NOTE: Invalid method") + sys.exit() + + + # response error if status code between these numbers + if (400 <= ret.status_code <=599): + + print(ret.text) + result=None + if stoponerror: sys.exit() + + # return the result + else: + # is it json + try: + result=ret.json() + except: + # is it text + try: + result=ret.text + except: + result=None + print("NOTE: No result to print") + + + + return result; + +# getfolderid +# when a Viya content path is passed in return the id, path and uri +# change history +# 01dec2017 initial development + +def getfolderid(path): + + # build the request parameters + reqval="/folders/folders/@item?path="+path + reqtype='get' + + callrestapi(reqval,reqtype) + + if result==None: + print("NOTE: Folder'"+path+"' not found.") + targetid=None + targetname=None + targeturi=None + else: + targetid=result['id'] + targetname=result['name'] + targeturi="/folders/folders/"+targetid + + return [targetid,targeturi,targetname] + + +# getbaseurl +# from the default profile return the baseurl of the Viya server +# change history +# 01dec2017 initial development +# 01jun2018 Deal with empty profile error +# 20nov2018 Use the SAS_CLI_PROFILE env variable + + +def getbaseurl(): + + # check that profile file is available and can be read + + # note the path to the profile is hard-coded right now + endpointfile=os.path.join(os.path.expanduser('~'),'.sas','config.json') + access_file=file_accessible(endpointfile,'r') + + #profile does not exist + if access_file==False: + print("ERROR: Cannot read CLI profile at:",endpointfile,". Recreate profile with sas-admin profile init.") + sys.exit() + + #profile is empty file + if os.stat(endpointfile).st_size==0: + print("ERROR: Cannot read CLI profile empty file at:",endpointfile,". Recreate profile with sas-admin profile init.") + sys.exit() + + # get json from profile + with open(endpointfile) as json_file: + data = json.load(json_file) + + # get the profile environment variable to use it + # if it is not set default to the default profile + + cur_profile=os.environ.get("SAS_CLI_PROFILE","Default") + #print("URL: ",cur_profile ) + + # check that information is in profile + if cur_profile in data: + baseurl=data[cur_profile]['sas-endpoint'] + else: + + baseurl=None + print("ERROR: profile "+cur_profile+" does not exist. Recreate profile with sas-admin profile init.") + sys.exit() + + + return baseurl + + +# getauthtoken +# from the stored auth file get the authentication token for the request header +# change history +# 01dec2017 initial development +# return oaval=None when no authtoken retrieved +# 20nov2018 Use the SAS_CLI_PROFILE env variable + +def getauthtoken(baseurl): + + + #get authentication information for the header + credential_file=os.path.join(os.path.expanduser('~'),'.sas','credentials.json') + + # check that credential file is available and can be read + access_file=file_accessible(credential_file,'r') + + if access_file==False: + oaval=None + print("ERROR: Cannot read authentication credentials at: ", credential_file) + print("ERROR: Try refreshing your token with sas-admin auth login") + sys.exit() + + with open(credential_file) as json_file: + data = json.load(json_file) + type(data) + + # the sas-admin profile init creates an empty credential file + # check that credential is in file, if it is add it to the header, if not exit + + # get the profile environment variable to use it + # if it is not set default to the default profile + + cur_profile=os.environ.get("SAS_CLI_PROFILE","Default") + + #print("LOGON: ", cur_profile ) + + if cur_profile in data: + + oauthToken=data[cur_profile]['access-token'] + + oauthTokenType="bearer" + + oaval=oauthTokenType + ' ' + oauthToken + + head= {'Content-type':'application/json','Accept':'application/json' } + head.update({"Authorization" : oaval}) + + # test a connection to rest api if it fails exit + r = requests.get(baseurl,headers=head) + + if (400 <= r.status_code <=599): + + oaval=None + print(r.text) + print("ERROR: cannot connect to "+baseurl+" is your token expired?") + print("ERROR: Try refreshing your token with sas-admin auth login") + sys.exit() + else: + + oaval=None + print("ERROR: access token not in file: ", credential_file) + print("ERROR: Try refreshing your token with sas-admin auth login") + sys.exit() + + return oaval + +# getinputjson +# load the returned json to a python dictionary +# change history +# 01dec2017 initial development + +def getinputjson(input_file): + + with open(input_file) as json_file: + inputdata = json.load(json_file) + + return inputdata + +# simpleresults +# take the complex json and create a simple print of the results +# change history +# 01dec2017 initial development + +def simpleresults(resultdata): + + # print a simplification of the json results + + + # list of items returned by rest call + if 'items' in resultdata: + + total_items=resultdata['count'] + + returned_items=len(resultdata['items']) + + if total_items == 0: print("Note: No items returned.") + + for i in range(0,returned_items): + + print ("=====Item ",i,"=======") + + pairs=resultdata['items'][i] + + for key,val in pairs.items(): + + if key != 'links': + print(key,end="") + print(" = ", val) + + print("Result Summary: Total items available: ",total_items ,"Total items returned: ", returned_items) + + elif 'id' in resultdata: #one item returned by rest call + + for key,val in resultdata.items(): + + if key != 'links': + print(key,end="") + print(" = ", val) + + else: + print("NOTE: No JSON Results Found") + + + +# tableresults +# take the complex json and create a simple table of the results +# change history +# 01aug2018 initial development + +def csvresults(resultdata): + + #print(resultdata) + + if 'items' in resultdata: + + total_items=resultdata['count'] + + returned_items=len(resultdata['items']) + + if total_items == 0: print("Note: No items returned.") + + for i in range(0,returned_items): + + pairs=resultdata['items'][i] + + #test=pairs.get('description') + #if test==None: pairs['description']='None' + + for key,val in pairs.items(): + + if i==0: print(key,',',end="") + + print("\n",end="") + + for key,val in pairs.items(): + + if key != 'links': + print('"',val,'",',end="") + + print("\n",end="") + + + elif 'id' in resultdata: #one item returned by rest call + + for key,val in resultdata.items(): + + if key != 'links': + print(key,',',end="") + + print("\n",end="") + + for key,val in resultdata.items(): + + if key != 'links': + print('"',val,'",',end="") + + print("\n",end="") + + else: + print("NOTE: No JSON Results Found") + + +# file_accessible +# Check if a file exists and is accessible. +# change history +# 01dec2017 initial development + +def file_accessible(filepath, mode): + + try: + f = open(filepath, mode) + f.close() + except IOError as e: + return False + + return True + + +# printresult +# prints the results in the style requested +# change history +# 28oct2018 initial development + +def printresult(result,output_style): + + # print rest call results + if type(result) is dict: + + if output_style=='simple': + simpleresults(result) + elif output_style=='csv': + csvresults(result) + else: + print(json.dumps(result,indent=2)) + else: print(result) + + + +# getprofileinfo +# prints the token expiration, endpoint and current user +# change history +# 20nov2018 initial development + + +def getprofileinfo(myprofile): + + + + #get authentication information for the header + credential_file=os.path.join(os.path.expanduser('~'),'.sas','credentials.json') + + # check that credential file is available and can be read + access_file=file_accessible(credential_file,'r') + + if access_file==False: + print("ERROR: Cannot read authentication credentials at: ", credential_file) + print("ERROR: Try refreshing your token with sas-admin auth login") + sys.exit() + + with open(credential_file) as json_file: + data = json.load(json_file) + type(data) + + # the sas-admin profile init creates an empty credential file + # check that credential is in file, if it is add it to the header, if not exit + + # get the profile environment variable to use it + # if it is not set default to the default profile + + + if myprofile in data: + + expiry=data[myprofile]['expiry'] + print("Note your authentication token expires at: "+expiry) + + else: + + print("ERROR: access token not in file: ", credential_file) + print("ERROR: Try refreshing your token with sas-admin auth login") + sys.exit() + + + # note the path to the profile is hard-coded right now + endpointfile=os.path.join(os.path.expanduser('~'),'.sas','config.json') + access_file=file_accessible(endpointfile,'r') + + #profile does not exist + if access_file==False: + print("ERROR: Cannot read CLI profile at:",endpointfile,". Recreate profile with sas-admin profile init.") + sys.exit() + + #profile is empty file + if os.stat(endpointfile).st_size==0: + print("ERROR: Cannot read CLI profile empty file at:",endpointfile,". Recreate profile with sas-admin profile init.") + sys.exit() + + # get json from profile + with open(endpointfile) as json_file: + data = json.load(json_file) + + + # check that information is in profile + if myprofile in data: + baseurl=data[myprofile]['sas-endpoint'] + print("Endpoint is: "+baseurl) + else: + print("ERROR: profile "+myprofile+" does not exist. Recreate profile with sas-admin profile init.") + + # build the request parameters + reqval="/identities/users/@currentUser" + reqtype='get' + + result=callrestapi(reqval,reqtype) + + if result==None: + print("NOTE: Not logged in.") + + else: + print("Logged on as id: "+ result['id']) + print("Logged on as name: "+result['name']) + + + \ No newline at end of file diff --git a/showsetup.py b/showsetup.py new file mode 100644 index 0000000..2d91aea --- /dev/null +++ b/showsetup.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# showsetup.py +# +# output some system settings to help with debugging issues +# +# October 2018 +# +# +# Change History +# +# +# 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 sys +import requests +import os + +from sharedfunctions import getprofileinfo + +# software versions +print("Python Version is: "+str(sys.version_info[0])+'.'+str(sys.version_info[1])) +print("Requests Version is: "+requests.__version__) + +# profile + +cur_profile=os.environ.get("SAS_CLI_PROFILE","NOTSET") + +if cur_profile=="NOTSET": + print("SAS_CLI_PROFILE environment variable not set, using Default profile") + cur_profile='Default' +else: + print("SAS_CLI_PROFILE environment variable set to profile "+ cur_profile) + + +ssl_file=os.environ.get("SSL_CERT_FILE","NOTSET") + +if ssl_file=="NOTSET": + print("SSL_CERT_FILE environment variable not set.") +else: + print("SSL_CERT_FILE environment variable set to profile "+ ssl_file) + + +r_ssl_file=os.environ.get("REQUESTS_CA_BUNDLE","NOTSET") + +if r_ssl_file=="NOTSET": + print("REQUESTS_CA_BUNDLE environment variable not set.") +else: + print("REQUESTS_CA_BUNDLE environment variable set to profile "+ r_ssl_file) + +getprofileinfo(cur_profile) diff --git a/testfolderaccess.py b/testfolderaccess.py new file mode 100644 index 0000000..a24d70e --- /dev/null +++ b/testfolderaccess.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# testfolderaccess.py +# January 2018 +# +# Usage: +# python testfolderaccess.py -f folderpath -n name_of_user_or_group -t user|group -s grant|prohibit -m permission [-q] [-d] +# +# +# +# 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. +# + +# CHANGE THIS VARIABLE IF YOUR CLI IS IN A DIFFERENT LOCATION +clidir='/opt/sas/viya/home/bin/' +debug=False + +# 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="Enter the path to the viya folder.",required='True') +parser.add_argument("-n","--name", help="Enter the name of the user or group to test.",required='True') +parser.add_argument("-t","--principaltype", help="Enter the type of principal to test: user or group.",required='True',choices=['user','group']) +parser.add_argument("-s","--setting", help="Enter grant or prohibit as the expected setting for the permission being tested.",required='True', choices=['grant','prohibit']) +parser.add_argument("-m","--permission", help="Enter the permission to test.",required='True', choices=["read","update","delete","add","secure","remove"]) +parser.add_argument("-q","--quiet", action='store_true') +parser.add_argument("-d","--debug", action='store_true') +args = parser.parse_args() +path_to_folder=args.folderpath +name=args.name +principaltype=args.principaltype +setting=args.setting +permission=args.permission +quiet=args.quiet +debug=args.debug + + +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]) + +endpoint='/authorization/decision' +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":[folder_uri]} + +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'][folder_uri] + +#print('e is a '+type(e).__name__+' object') #e is a list object + +principal_found=False + +for pi in e: + #print pi['principal'] + # Test whether principal has a name: authenticatedusers and guest do not have a name key + if 'name' in pi['principal']: + if (pi['principal']['name'].lower() == name.lower()): + #print(pi['principal']['name']+':'+pi[permission.lower()]['result']) + principal_found=True + if (pi[permission.lower()]['result'] == setting.lower()): + if not quiet: + print('TEST PASSED: the effective '+permission.lower()+' permission for '+pi['principal']['name']+' on folder '+path_to_folder+' is '+pi[permission.lower()]['result']) + else: + raise Exception('TEST FAILED: the effective '+permission.lower()+' permission for '+pi['principal']['name']+' on folder '+path_to_folder+' is '+pi[permission.lower()]['result']+', not '+setting.lower()) + +if not principal_found: + raise Exception('No direct or inherited authorization rules found for \''+name+'\' on folder '+path_to_folder+'. Please check that you spelled the principal name correctly, and specified the correct principal type - user or group.') diff --git a/unittestsadm33.sh b/unittestsadm33.sh new file mode 100644 index 0000000..3dc8374 --- /dev/null +++ b/unittestsadm33.sh @@ -0,0 +1,139 @@ +#!/usr/bin/sh +# +# unittestsadm33.sh +# June 2018 +# +# Calls each of the pyviyatools at least once, as a simple unit/integration test +# +# Some tests are provided with example folder paths which are not likely to +# exist in your deployment. However, most tests are not dependent on any +# custom content in the deployment, and will run well on any deployment. +# +# Some tests intentionally do things which do not work, e.g. delete a folder +# which does not exist. The error message returned by the tool called is +# considered sufficient to demonstrate that it has in fact been called, and is +# working as intended. If you like, you could create content for these tests +# to act on, e.g. create a folder called "/this_folder_does_not_exist", and +# allow one of the tests below delete it. +# +# The following tests create new content, and do not clean up after themselves: +# 1. "Create a domain using createdomain" +# - creates or replaces domain named 'test', does not create multiple +# copies +# 2. "Create a binary backup job" +# - creates a new scheduled job named 'BINARY_BACKUP_JOB' each time it +# runs, will create multiple copies +# You may wish to clean up after them manually, especially in a +# real customer environment. Study the tests and/or run them individually +# to learn more about what they create, so that you can find and delete it +# yourself. In a dev, PoC, playpen or classroom environment, the cleanup +# might be optional, as the created objects will not interfere with other +# content or work. +# +# Change History +# +# 01Jun2018 Initial version after refactoring tools +# 18oct2018 updated gerrulid test because -o changed to -u +# +# +# 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 +# +# https://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. + + +echo "Return all the rest calls that can be made to the folders endpoint" +./callrestapi.py -e /folders -m get +echo + +echo "Return the json for all the folders/folders" +./callrestapi.py -e /folders/folders -m get +echo + +echo "Return simple text for all the folders/folders" +./callrestapi.py -e /folders/folders -m get -o simple +echo + +echo "Rest calls often limit the results returned the text output will tell you returned and total available items" +echo "in this call set a limit above the total items to see everything" +./callrestapi.py -e /folders/folders?limit=500 -m get -o simple +echo + +echo "Return the json for all the identities" +./callrestapi.py -e /identities/identities -m get +echo + +echo "Return the json for all the identities output to a file" +./callrestapi.py -e /identities/identities -m get > /tmp/identities.json +echo +echo "Contents of /tmp/identities.json:" +cat /tmp/identities.json +echo "End of contents of /tmp/identities.json" +echo +echo "Deleting /tmp/identities.json" +rm /tmp/identities.json +echo "Demonstrating that /tmp/identities.json has been deleted - list it, ls should say no such file or directory:" +ls -al /tmp/identities.json +echo + +echo "Refresh the identities cache" +./callrestapi.py -e /identities/userCount -m get +./callrestapi.py -e /identities/cache/refreshes -m post +echo + +echo "Pass the folder path and return the folder id and uri" +./getfolderid.py -f /gelcontent +echo + +echo "Delete a folder based on its path - we don't want to delete a real folder, so try (and fail) to delete one which does not exist" +./deletefolder.py -f /this_folder_does_not_exist +echo + +echo "Delete a folder and its content - we don't want to delete a real folder, so try (and fail) to delete one which does not exist" +./deletefolderandcontent.py -f /this_folder_does_not_exist +echo + +echo "Return a set of configuration properties" +./getconfigurationproperties.py -c sas.identities.providers.ldap.user +echo + +echo "Create a domain using createdomain" +./createdomain.py -t password -d test -u sasadm -p lnxsas -g "SASAdministrators,HRs,Sales" +echo + +echo "Create a binary backup job" +./createbinarybackup.py +echo + +echo "Get a rule ID" +#Get /Public folder ID +./getfolderid.py --folderpath /Public > /tmp/folderid.txt +id=$(grep "Id " /tmp/folderid.txt | tr -s ' ' | cut -f3 -d " ") +echo "The Public folder ID is" $id +./getruleid.py -u /folders/folders/$id/** -p authenticatedUsers +echo + +echo "Move all content from one folder to another folder (or in this case, the same folder)" +./movecontent.py -s /gelcontent/GELCorp/Shared/Reports -t /gelcontent/GELCorp/Shared/Reports -q +echo + +echo "Test folder access" +./testfolderaccess.py -f '/gelcontent/GELCorp' -n gelcorp -t group -m read -s grant +echo + +echo "Display all sasadministrator rules" +./listrules.py --p SASadministrators -o simple +echo + +echo "Display all rules that contain SASVisual in the URI" +./listrules.py -u SASVisual -o simple +echo diff --git a/unittestsadm34.sh b/unittestsadm34.sh new file mode 100644 index 0000000..a987659 --- /dev/null +++ b/unittestsadm34.sh @@ -0,0 +1,181 @@ +#!/usr/bin/sh +# +# unittestsadm33.sh +# December 2018 +# +# Calls each of the pyviyatools at least once, as a simple unit/integration test +# +# Some tests are provided with example folder paths which are not likely to +# exist in your deployment. However, most tests are not dependent on any +# custom content in the deployment, and will run well on any deployment. +# +# Some tests intentionally do things which do not work, e.g. delete a folder +# which does not exist. The error message returned by the tool called is +# considered sufficient to demonstrate that it has in fact been called, and is +# working as intended. If you like, you could create content for these tests +# to act on, e.g. create a folder called "/this_folder_does_not_exist", and +# allow one of the tests below delete it. +# +# The following tests create new content, and do not clean up after themselves: +# 1. "Create a domain using createdomain" +# - creates or replaces domain named 'test', does not create multiple +# copies +# 2. "Create a binary backup job" +# - creates a new scheduled job named 'BINARY_BACKUP_JOB' each time it +# runs, will create multiple copies +# You may wish to clean up after them manually, especially in a +# real customer environment. Study the tests and/or run them individually +# to learn more about what they create, so that you can find and delete it +# yourself. In a dev, PoC, playpen or classroom environment, the cleanup +# might be optional, as the created objects will not interfere with other +# content or work. +# +# Change History +# +# 01Jun2018 Initial version after refactoring tools +# 18Oct2018 updated gerrulid test because -o changed to -u +# 03Dec2018 Added tests for explainaccess.py +# +# +# 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 +# +# https://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. + + +echo "Return all the rest calls that can be made to the folders endpoint" +./callrestapi.py -e /folders -m get +echo + +echo "Return the json for all the folders/folders" +./callrestapi.py -e /folders/folders -m get +echo + +echo "Return simple text for all the folders/folders" +./callrestapi.py -e /folders/folders -m get -o simple +echo + +echo "Rest calls often limit the results returned the text output will tell you returned and total available items" +echo "in this call set a limit above the total items to see everything" +./callrestapi.py -e /folders/folders?limit=500 -m get -o simple +echo + +echo "Return the json for all the identities" +./callrestapi.py -e /identities/identities -m get +echo + +echo "Return the json for all the identities output to a file" +./callrestapi.py -e /identities/identities -m get > /tmp/identities.json +echo +echo "Contents of /tmp/identities.json:" +cat /tmp/identities.json +echo "End of contents of /tmp/identities.json" +echo +echo "Deleting /tmp/identities.json" +rm /tmp/identities.json +echo "Demonstrating that /tmp/identities.json has been deleted - list it, ls should say no such file or directory:" +ls -al /tmp/identities.json +echo + +echo "Refresh the identities cache" +./callrestapi.py -e /identities/userCount -m get +./callrestapi.py -e /identities/cache/refreshes -m post +echo + +echo "Pass the folder path and return the folder id and uri" +./getfolderid.py -f /gelcontent +echo + +echo "Delete a folder based on its path - we don't want to delete a real folder, so try (and fail) to delete one which does not exist" +./deletefolder.py -f /this_folder_does_not_exist +echo + +echo "Delete a folder and its content - we don't want to delete a real folder, so try (and fail) to delete one which does not exist" +./deletefolderandcontent.py -f /this_folder_does_not_exist +echo + +echo "Return a set of configuration properties" +./getconfigurationproperties.py -c sas.identities.providers.ldap.user +echo + +echo "Create a domain using createdomain" +./createdomain.py -t password -d test -u sasadm -p lnxsas -g "SASAdministrators,HR,Sales" +echo + +# Commented out for Viya 3.4 version: this tool is only intended for use with Viya 3.3 +#echo "Create a binary backup job" +#./createbinarybackup.py +#echo + +echo "Get a rule ID" +#Get /Public folder ID +./getfolderid.py --folderpath /Public > /tmp/folderid.txt +id=$(grep "Id " /tmp/folderid.txt | tr -s ' ' | cut -f3 -d " ") +echo "The Public folder ID is" $id +./getruleid.py -u /folders/folders/$id/** -p authenticatedUsers +echo + +echo "Move all content from one folder to another folder (or in this case, the same folder)" +./movecontent.py -s /gelcontent/GELCorp/Shared/Reports -t /gelcontent/GELCorp/Shared/Reports -q +echo + +echo "Test folder access" +./testfolderaccess.py -f '/gelcontent/GELCorp' -n gelcorp -t group -m read -s grant +echo + +echo "Display all sasadministrator rules" +./listrules.py --p SASadministrators -o simple +echo + +echo "Display all rules that contain SASVisual in the URI" +./listrules.py -u SASVisual -o simple +echo + +echo "Create folders from a CSV file" +./createfolders.py -h +echo + +echo Update the theme for the user sasadm +./updatepreferences.py -t user -tn sasadm -pi OpenUI.Theme.Default -pv sas_hcb +echo + +echo "Explain permissions for the folder /gelcontent/GELCorp, with no header row." +./explainaccess.py -f /gelcontent/GELCorp +echo + +echo "Explain permissions for the folder /gelcontent/GELCorp, with no header row, for Heather." +./explainaccess.py -f /gelcontent/GELCorp -n Heather -t user +echo + +echo "Explain permissions for the folder /gelcontent/GELCorp, with a header row, and include the folder path." +./explainaccess.py -f /gelcontent/GELCorp --header -p +echo + +echo "Explain permissions for the folder /gelcontent/GELCorp, showing only rows with at least one direct permission." +./explainaccess.py -f /gelcontent/GELCorp --direct_only +echo + +echo "Explain permissions for several application capabilities. Notice that there are no conveyed permissions in the results" +./explainaccess.py -u /SASEnvironmentManager/dashboard --header -p -l read update delete secure add remove create +./explainaccess.py -u /SASDataStudio/** -p -l read update delete secure add remove create +./explainaccess.py -u /SASDataExplorer/** -p -l read update delete secure add remove create +./explainaccess.py -u /SASVisualAnalytics_capabilities/buildAnalyticalModel -p -l read update delete secure add remove create +echo + +echo "Explain the permissions for a folder expressed as a URI. This works with any kind of object, not just folders" +echo "Uses the default permissions list. Specify -c true to include conveyed permissions, as they are not included by default when you use the -u URI parameter." +#Get /gelcontent/GELCorp folder ID +./getfolderid.py --folderpath /gelcontent/GELCorp > /tmp/folderid.txt +id=$(grep "Id " /tmp/folderid.txt | tr -s ' ' | cut -f3 -d " ") +./explainaccess.py -u /folders/folders/$id --header -p -c true +echo + diff --git a/updatedomain.py b/updatedomain.py new file mode 100644 index 0000000..7822b64 --- /dev/null +++ b/updatedomain.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# updatedomain.py +# November 2018 +# +# update a viya domain to add credentials from a csv +# the domain must exist +# +# Change History +# +# 27JAN2017 Comments added +# 03DEC2018 Strip spaces from the end of users and groups +# +# csv file format +# no header row +# column1 is userid +# column2 is password +# column3 is identity +# column4 is identity type (user or group) +# For example: +# myuserid,mypass,Sales,group +# acct1,pw1,Admin,user +# etc +# +# 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 +# +# https://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. + + +# Update a domain + +import csv +import base64 +import argparse + +from sharedfunctions import callrestapi, file_accessible + +parser = argparse.ArgumentParser(description="Update a Viya Domain to add credentials from a csv file.") +parser.add_argument("-d","--domain", help="Existing Domain.",required=True) +parser.add_argument("-f","--file", help="A csv file containing groups and userids.",required=True) + +args = parser.parse_args() + +domain_name=args.domain +file=args.file + +# check that domain exists +reqval="/credentials/domains/"+domain_name +reqtype="get" + +#if domain does not exist call restapi will exit and no additional code is run +domainexist=callrestapi(reqval,reqtype) + +type=domainexist['type'] + +# read the csv file to create json +check=file_accessible(file,'r') + +# file can be read +if check: + + with open(file, 'rt') as f: + filecontents = csv.reader(f) + for row in filecontents: + + #print(row) + + userid=row[0] + pwval=row[1] + ident=row[2].rstrip() + identtype=row[3].rstrip() + + + if pwval: cred=base64.b64encode(pwval.encode("utf-8")).decode("utf-8") + + if identtype=="group": end_ident="groups" + if identtype=="user": end_ident="users" + + reqval="/credentials/domains/"+domain_name+"/"+end_ident+"/"+ident + reqtype="put" + + data = {} + data['domainId'] = domain_name + data['domainType'] = type + data['identityId'] = ident + data['identityType'] = identtype + data['properties']={"userId": userid} + if pwval: + data['secrets']={"password": cred} + + #print(reqval) + #print(data) + + # make the rest call + callrestapi(reqval,reqtype,data=data) +else: + print("ERROR: cannot read "+file) diff --git a/updatepreferences.py b/updatepreferences.py new file mode 100644 index 0000000..0b633b8 --- /dev/null +++ b/updatepreferences.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# updatepreferences.py +# October 2018 +# +# Update user preferences for a single user of a group of users +# +# Change History +# +# 30OCT2018 first version +# +# 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 +# +# https://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. + + +#################################################################### +#### COMMAND LINE EXAMPLE #### +#################################################################### +#### ./updatepreferences.py - #### +#### -t user #### +#### -tn myUser #### +#### -pi VA.geo.drivedistance.unit #### +#### -pv kilometers #### +#################################################################### +#### POSSIBLE VALUES #### +#################################################################### +#### sas.welcome.suppress = true/false #### +#### sas.drive.show.pinned = true/false #### +#### VA.geo.drivedistance.unit = kilometers/miles #### +#### OpenUI.Theme.Default = sas_corporate/sas_inspire/sas_hcb #### +#################################################################### + +import argparse +from sharedfunctions import callrestapi + +parser = argparse.ArgumentParser(description="Update user preferences for a user or a group of users") +parser.add_argument("-t", "--target", help="Type the target of the update: user or group", required=True, choices=['user', 'group']) +parser.add_argument("-tn", "--targetname", help="ID of the user or group to which the update applies.", required=True) +parser.add_argument("-pi", "--preferenceid", help="ID of the preference to be updated", required=True) +parser.add_argument("-pv", "--preferencevalue", help="Value to be set for the preference", required=True) + +args = parser.parse_args() +target = args.target +targetName = args.targetname +preferenceID = args.preferenceid +preferenceValue = args.preferencevalue + +json= {"application": "SAS Visual Analytics", "version": 1,"id": preferenceID ,"value": preferenceValue} + +# Function to update preference of a specific user +if target == 'user' : + + userID=targetName + + reqtype='get' + reqval="/identities/users/"+userID + + userexist=callrestapi(reqval,reqtype) + + reqtype="put" + reqval="/preferences/preferences/"+ userID +"/" + preferenceID + result=callrestapi(reqval,reqtype,data=json) + print("Updating Preference "+reqval+" = "+preferenceValue) + + +else: # Execute actual code to update the preference for a user or a group + + reqtype='get' + reqval='/identities/groups/'+ targetName +'/members?limit=1000' + resultdata=callrestapi(reqval,reqtype) + + reqtype="put" + + if 'items' in resultdata: + + returned_items=len(resultdata['items']) + for i in range(0,returned_items): + + id=resultdata['items'][i]['id'] + type=resultdata['items'][i]['type'] + + if type=="user": + reqval="/preferences/preferences/"+ id +"/" + preferenceID + result=callrestapi(reqval, reqtype,data=json,stoponerror=0) + print(result) + print("Updating Preference "+reqval+" = "+preferenceValue) + else: print("Cannot set preferences for a group "+id ) \ No newline at end of file