From aef1c56df20a0a5e5faf53083205333d1175f7cf Mon Sep 17 00:00:00 2001 From: Gerry Nelson <45608078+gerrynelson63@users.noreply.github.com> Date: Thu, 6 Dec 2018 13:26:25 -0500 Subject: [PATCH] Initial commit of code --- ContributorAgreement.txt | 111 ++++++++ call_rest_api.py | 32 +++ callrestapi.py | 76 +++++ createbinarybackup.py | 131 +++++++++ createdomain.py | 97 +++++++ createfolders.py | 93 ++++++ deletefolder.py | 79 ++++++ deletefolderandcontent.py | 103 +++++++ explainaccess.py | 293 +++++++++++++++++++ getconfigurationproperties.py | 52 ++++ getfolderid.py | 47 +++ getruleid.py | 59 ++++ listrules.py | 72 +++++ loginviauthinfo.py | 59 ++++ movecontent.py | 99 +++++++ sharedfunctions.py | 521 ++++++++++++++++++++++++++++++++++ showsetup.py | 65 +++++ testfolderaccess.py | 114 ++++++++ unittestsadm33.sh | 139 +++++++++ unittestsadm34.sh | 181 ++++++++++++ updatedomain.py | 108 +++++++ updatepreferences.py | 99 +++++++ 22 files changed, 2630 insertions(+) create mode 100644 ContributorAgreement.txt create mode 100644 call_rest_api.py create mode 100644 callrestapi.py create mode 100644 createbinarybackup.py create mode 100644 createdomain.py create mode 100644 createfolders.py create mode 100644 deletefolder.py create mode 100644 deletefolderandcontent.py create mode 100644 explainaccess.py create mode 100644 getconfigurationproperties.py create mode 100644 getfolderid.py create mode 100644 getruleid.py create mode 100644 listrules.py create mode 100644 loginviauthinfo.py create mode 100644 movecontent.py create mode 100644 sharedfunctions.py create mode 100644 showsetup.py create mode 100644 testfolderaccess.py create mode 100644 unittestsadm33.sh create mode 100644 unittestsadm34.sh create mode 100644 updatedomain.py create mode 100644 updatepreferences.py 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