Skip to content

Commit

Permalink
Merge pull request bryanthowell-tableau#90 from bryantbhowell/5.2.0
Browse files Browse the repository at this point in the history
5.2.0
  • Loading branch information
Bryant Howell authored May 28, 2020
2 parents a373943 + 11f2f21 commit 1cb0b69
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 16 deletions.
1 change: 1 addition & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
#from .tableau._server_rest import TableauServerRest, TableauServerRest33
from .tableau_rest_api_connection import *
from .tableau_server_rest import *
from .rest_tokens_manager import *
175 changes: 175 additions & 0 deletions examples/limited_rest_api_wrapping_tableau_rest_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from tableau_tools import *
#import time

# This script shows two example generic functions which utilize the RestTokensManager class
# It is an example of how you can create a wrapper REST API which exposes some of the
# Tableau REST API functionality but not all of it, while using the impersonation feature
# so that each request is performed for the User, without needing their credentials (only admin credentials)
# Taken from a Django project, but Flask code would work similarly

# You would probably store your admin credentials securely in an settings or ENV file
server = settings.TABLEAU_SERVER
admin_username = settings.TABLEAU_ADMIN_USERNAME
admin_password = settings.TABLEAU_ADMIN_PASSWORD
# This is most likely just 'default' but you might have some reason not to bootstrap from there
default_site_content_url = settings.TABLEAU_DEFAULT_SITE_CONTENT_URL

tableau_tools_logger = Logger('tableau_tools.log')

# Connect to the default site to bootstrap the process

# In a running app server, the same process is always running so it needs a Master login PER site
# But also, REST API sessions do timeout, so we need to check for that possibility and remove
# Sessions once they have timed out

# You must use an Admin Username and Password, as PAT does not have Impersonation at this time (2020.2)
d = TableauServerRest32(server=server, username=admin_username, password=admin_password,
site_content_url=default_site_content_url)
# alternatively could use the older TableauRestApiConnection objects if you had code built on those objects

# If you are using a self-signed cert or need to pass in a CERT chain, this pass directly to
# the requests library https://requests.readthedocs.io/en/master/user/quickstart/ to do whatever SSL option you need:
# d.verify_ssl_cert = False

d.enable_logging(tableau_tools_logger)
# Other options you might turn off for deeper logging:
# tableau_tools_logger.enable_request_logging()
# tableau_tools_logger.enable_response_logging()
# tableau_tools_logger.enable_debug_level()


# This manages all the connection tokens here on out
connections = RestTokensManager()

#
# RestTokensManager methods are all functional -- you pass in a TableauServerRest or TableauRestApiConnection object
# and then it perhaps actions on that object, such as logging in as a different user or switching to
# an already logged in user.
# Internally it maintains a data structure with the Admin tokens for any site that has been signed into
# And the individual User Tokens for any User / Site combination that has been signed into
# It does not RUN any REST API commands other than sign-in: You run those commands on the
# connection object once it has been returned

# For example, once this is run, the connection object 'd' will have been signed in, and you can
# do any REST API command against 'd', and it will be done as the master on the default site
# This is just bootstrapping at the very beginning to make sure we've connected successfully
# with the admin credentials. If there are errors at this point, something is likely wrong
# with the configuration/credentials or the Tableau Server
default_token = connections.sign_in_connection_object(d)

# Next is a generic_request function (based on Django pattern), that utilizes the connections object

# Every one of our REST methods follows basically this pattern
# So it has been made generic
# You pass the callback function to do whatever you want with
# the REST API object and whatever keyword arguments it needs
# Callback returns a valid type of HttpResponse object and we're all good
def generic_request(request, site, callback_function, **kwargs):
# Generic response to start. This will be returned if no other condition overwrites it
response = HttpResponseServerError()

# If request is none, then it is an admin level function
if request is not None:
# Check user, if non, response is Http Forbidden
# This function represents whatever your application needs to do to tell you the user who has logged in securely
username = check_user_session(request)
if username is None:
response = HttpResponseForbidden()
return response
else:
# If username is none, the request is run as the Site Admin
username = None

# Create Connection Object for Given User
# Just create, but don't sign in. Will use swap via the TokenManager
t = TableauServerRest32(server=server, username=admin_username, password=admin_password,
site_content_url=default_site_content_url)

# Again, you might need to pass in certain arguments to requests library if using a self-signed cert
#t.verify_ssl_cert = False
t.enable_logging(tableau_tools_logger)

# Check for connection, attempt to reestablish if possible
if connections.connection_signed_in is False:
tableau_tools_logger.log("Signing back in to the master user")
# If the reconnection fails, return Server Error response
if connections.sign_in_connection_object(rest_connection=t) is False:
# This is a Django error response, take it as whatever HTTP error you'd like to throw
response = HttpResponseServerError()
# If connection is already confirmed, just swap to the user token for the site
else:
# Site Admin level request
if username is None:
tableau_tools_logger.log("Swapping to Site Admin ")
connections.switch_to_site_master(rest_connection=t, site_content_url=site)
tableau_tools_logger.log("Token is now {}".format(t.token))
# Request as a particular username
else:
tableau_tools_logger.log("Swapping in existing user token for user {}".format(username))
connections.switch_user_and_site(rest_connection=t, username=username, site_content_url=site)
tableau_tools_logger.log("Token is now {}".format(t.token))

# Do action with connection
# Whatever callback function was specified will be called with RestApiConnection / TableauServerRest object as first argument
# then any other kwargs in the order they were passed.
# The callback function must return a Django HttpResponse (or related) object
# But within the callback, 't' is the TableauServerRest or TableauRestApiConnection object with the token for the
# particular user you want
try:
response = callback_function(t, **kwargs)

except NotSignedInException as e:
if username is None:
tableau_tools_logger.log("Master REST API session on site {} has timed out".format(site))
del connections.site_master_tokens[site]
# Rerun the connection
tableau_tools_logger.log("Creating new user token for site master")
connections.switch_to_site_master(rest_connection=t, site_content_url=site)
tableau_tools_logger.log("Token is now {}".format(t.token))
else:
tableau_tools_logger.log("User {} REST API session on vertical {} has timed out".format(username, site))
del connections.site_user_tokens[site][username]
# Rerun the connection
tableau_tools_logger.log("Creating new user token for username {} on vertical {}".format(username, site))
connections.switch_user_and_site(rest_connection=t, username=username, site_content_url=site)
tableau_tools_logger.log("Token is now {}".format(t.token))
# Rerun the orginal callback command
tableau_tools_logger.log("Doing callback function again now that new token exists")
response = callback_function(t, **kwargs)
# Originally, the code looked at the following two exceptions. This is been replaced by looking at NotSignedInException
# RecoverableHTTPException is an exception from tableau_tools, when it is known what the error represents
# HTTPError is a Requests library exception, which might happen if tableau_tools doesn't wrap the particular error.
# except (RecoverableHTTPException, HTTPError) as e:
# if e.http_code == 401:
except Exception as e:
raise e
# Destroy REST API Connection Object, which is just used within this code block
del t
# Return Response
return response

# There were originally separate functions but they shared enough code to be merged together
def admin_request(request, site, callback_function, **kwargs):
# We don't pass the 'request' here, because it would have the end user's username attached via the session
# The point is that username ends up None in the generic_request call, forcing it to use the admin
return generic_request(None, site, callback_function, **kwargs)


#
# Here is an example of an actual exposed endpoint
#

# This is what is passed in as the callback function - so rest_connection is the 't' object passed in by generic_request
# Returns all of the Projects a user can see content in, alphabetically sorted
def query_projects(rest_connection: TableauServerRest32):
p_sort = Sort('name', 'asc')
p = rest_connection.query_projects_json(sorts=[p_sort, ])
return JsonResponse(p)

# An exposed endpoint linked to an actual URL
def projects(request, site):
#log("Starting to request all workbooks")
# Note we are just wrapping the generic request (this one doesn't take keyword arguments, but anything after
# 'query_projects' would be passed as an argument into the query_projects function (if it took arguments)
response = generic_request(request, site, query_projects)
return response
26 changes: 13 additions & 13 deletions examples/permissions_auditing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
default.enable_logging(logger)
default.signin()

with open('permissions_audit.txt', 'wb') as output_file:
with open('permissions_audit.txt', 'w', newline='') as output_file:

# Get all sites content urls for logging in
site_content_urls = default.query_all_site_content_urls()
Expand All @@ -23,15 +23,15 @@
headers = ['Site Content URL', 'Project Name', 'Project LUID', 'Are Permissions Locked?',
'Principal Type', 'Principal Name', 'Principal LUID']

project_caps = default.available_capabilities[default.api_version]['project']
project_caps = Permissions.available_capabilities[default.api_version]['project']
for cap in project_caps:
headers.append(',{}'.format(cap))
workbook_caps = default.available_capabilities[default.api_version]['workbook']
headers.append(cap)
workbook_caps = Permissions.available_capabilities[default.api_version]['workbook']
for cap in workbook_caps:
headers.append(',{}'.format(cap))
datasource_caps = default.available_capabilities[default.api_version]['datasource']
headers.append(cap)
datasource_caps = Permissions.available_capabilities[default.api_version]['datasource']
for cap in datasource_caps:
headers.append(',{}'.format(cap))
headers.append(cap)
output_writer.writerow(headers)

for site_content_url in site_content_urls:
Expand All @@ -53,13 +53,13 @@
all_perms_list = proj_obj.convert_all_permissions_to_list(all_perms[luid])
if site_content_url is None:
site_content_url = ''
output_row.append(site_content_url.encode('utf-8'))
output_row.append(project.encode('utf-8'))
output_row.append(projects_dict[project].encode('utf-8'))
output_row.append(site_content_url)
output_row.append(project)
output_row.append(projects_dict[project])
output_row.append(str(proj_obj.are_permissions_locked()))
output_row.append(all_perms[luid]["type"].encode('utf-8'))
output_row.append(all_perms[luid]["name"].encode('utf-8'))
output_row.append(luid.encode('utf-8'))
output_row.append(all_perms[luid]["type"])
output_row.append(all_perms[luid]["name"])
output_row.append(luid)
output_row.extend(all_perms_list)
output_writer.writerow(output_row)

Expand Down
2 changes: 1 addition & 1 deletion examples/template_publish_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def __init__(self, orig_content_url):
temp_wb_file = t_file.save_new_file('Modified Workbook'.format(wb))
new_workbook_luid = d.workbooks.publish_workbook(workbook_filename=temp_wb_file, workbook_name=wb,
project_obj=dest_project,
overwrite=True, check_published_ds=False)
overwrite=True)
print('Published new workbook {}'.format(new_workbook_luid))
os.remove(temp_wb_file)

Expand Down
Loading

0 comments on commit 1cb0b69

Please sign in to comment.