Skip to content

Commit 1cb0b69

Browse files
author
Bryant Howell
authored
Merge pull request #90 from bryantbhowell/5.2.0
5.2.0
2 parents a373943 + 11f2f21 commit 1cb0b69

File tree

7 files changed

+407
-16
lines changed

7 files changed

+407
-16
lines changed

__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
#from .tableau._server_rest import TableauServerRest, TableauServerRest33
99
from .tableau_rest_api_connection import *
1010
from .tableau_server_rest import *
11+
from .rest_tokens_manager import *
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from tableau_tools import *
2+
#import time
3+
4+
# This script shows two example generic functions which utilize the RestTokensManager class
5+
# It is an example of how you can create a wrapper REST API which exposes some of the
6+
# Tableau REST API functionality but not all of it, while using the impersonation feature
7+
# so that each request is performed for the User, without needing their credentials (only admin credentials)
8+
# Taken from a Django project, but Flask code would work similarly
9+
10+
# You would probably store your admin credentials securely in an settings or ENV file
11+
server = settings.TABLEAU_SERVER
12+
admin_username = settings.TABLEAU_ADMIN_USERNAME
13+
admin_password = settings.TABLEAU_ADMIN_PASSWORD
14+
# This is most likely just 'default' but you might have some reason not to bootstrap from there
15+
default_site_content_url = settings.TABLEAU_DEFAULT_SITE_CONTENT_URL
16+
17+
tableau_tools_logger = Logger('tableau_tools.log')
18+
19+
# Connect to the default site to bootstrap the process
20+
21+
# In a running app server, the same process is always running so it needs a Master login PER site
22+
# But also, REST API sessions do timeout, so we need to check for that possibility and remove
23+
# Sessions once they have timed out
24+
25+
# You must use an Admin Username and Password, as PAT does not have Impersonation at this time (2020.2)
26+
d = TableauServerRest32(server=server, username=admin_username, password=admin_password,
27+
site_content_url=default_site_content_url)
28+
# alternatively could use the older TableauRestApiConnection objects if you had code built on those objects
29+
30+
# If you are using a self-signed cert or need to pass in a CERT chain, this pass directly to
31+
# the requests library https://requests.readthedocs.io/en/master/user/quickstart/ to do whatever SSL option you need:
32+
# d.verify_ssl_cert = False
33+
34+
d.enable_logging(tableau_tools_logger)
35+
# Other options you might turn off for deeper logging:
36+
# tableau_tools_logger.enable_request_logging()
37+
# tableau_tools_logger.enable_response_logging()
38+
# tableau_tools_logger.enable_debug_level()
39+
40+
41+
# This manages all the connection tokens here on out
42+
connections = RestTokensManager()
43+
44+
#
45+
# RestTokensManager methods are all functional -- you pass in a TableauServerRest or TableauRestApiConnection object
46+
# and then it perhaps actions on that object, such as logging in as a different user or switching to
47+
# an already logged in user.
48+
# Internally it maintains a data structure with the Admin tokens for any site that has been signed into
49+
# And the individual User Tokens for any User / Site combination that has been signed into
50+
# It does not RUN any REST API commands other than sign-in: You run those commands on the
51+
# connection object once it has been returned
52+
53+
# For example, once this is run, the connection object 'd' will have been signed in, and you can
54+
# do any REST API command against 'd', and it will be done as the master on the default site
55+
# This is just bootstrapping at the very beginning to make sure we've connected successfully
56+
# with the admin credentials. If there are errors at this point, something is likely wrong
57+
# with the configuration/credentials or the Tableau Server
58+
default_token = connections.sign_in_connection_object(d)
59+
60+
# Next is a generic_request function (based on Django pattern), that utilizes the connections object
61+
62+
# Every one of our REST methods follows basically this pattern
63+
# So it has been made generic
64+
# You pass the callback function to do whatever you want with
65+
# the REST API object and whatever keyword arguments it needs
66+
# Callback returns a valid type of HttpResponse object and we're all good
67+
def generic_request(request, site, callback_function, **kwargs):
68+
# Generic response to start. This will be returned if no other condition overwrites it
69+
response = HttpResponseServerError()
70+
71+
# If request is none, then it is an admin level function
72+
if request is not None:
73+
# Check user, if non, response is Http Forbidden
74+
# This function represents whatever your application needs to do to tell you the user who has logged in securely
75+
username = check_user_session(request)
76+
if username is None:
77+
response = HttpResponseForbidden()
78+
return response
79+
else:
80+
# If username is none, the request is run as the Site Admin
81+
username = None
82+
83+
# Create Connection Object for Given User
84+
# Just create, but don't sign in. Will use swap via the TokenManager
85+
t = TableauServerRest32(server=server, username=admin_username, password=admin_password,
86+
site_content_url=default_site_content_url)
87+
88+
# Again, you might need to pass in certain arguments to requests library if using a self-signed cert
89+
#t.verify_ssl_cert = False
90+
t.enable_logging(tableau_tools_logger)
91+
92+
# Check for connection, attempt to reestablish if possible
93+
if connections.connection_signed_in is False:
94+
tableau_tools_logger.log("Signing back in to the master user")
95+
# If the reconnection fails, return Server Error response
96+
if connections.sign_in_connection_object(rest_connection=t) is False:
97+
# This is a Django error response, take it as whatever HTTP error you'd like to throw
98+
response = HttpResponseServerError()
99+
# If connection is already confirmed, just swap to the user token for the site
100+
else:
101+
# Site Admin level request
102+
if username is None:
103+
tableau_tools_logger.log("Swapping to Site Admin ")
104+
connections.switch_to_site_master(rest_connection=t, site_content_url=site)
105+
tableau_tools_logger.log("Token is now {}".format(t.token))
106+
# Request as a particular username
107+
else:
108+
tableau_tools_logger.log("Swapping in existing user token for user {}".format(username))
109+
connections.switch_user_and_site(rest_connection=t, username=username, site_content_url=site)
110+
tableau_tools_logger.log("Token is now {}".format(t.token))
111+
112+
# Do action with connection
113+
# Whatever callback function was specified will be called with RestApiConnection / TableauServerRest object as first argument
114+
# then any other kwargs in the order they were passed.
115+
# The callback function must return a Django HttpResponse (or related) object
116+
# But within the callback, 't' is the TableauServerRest or TableauRestApiConnection object with the token for the
117+
# particular user you want
118+
try:
119+
response = callback_function(t, **kwargs)
120+
121+
except NotSignedInException as e:
122+
if username is None:
123+
tableau_tools_logger.log("Master REST API session on site {} has timed out".format(site))
124+
del connections.site_master_tokens[site]
125+
# Rerun the connection
126+
tableau_tools_logger.log("Creating new user token for site master")
127+
connections.switch_to_site_master(rest_connection=t, site_content_url=site)
128+
tableau_tools_logger.log("Token is now {}".format(t.token))
129+
else:
130+
tableau_tools_logger.log("User {} REST API session on vertical {} has timed out".format(username, site))
131+
del connections.site_user_tokens[site][username]
132+
# Rerun the connection
133+
tableau_tools_logger.log("Creating new user token for username {} on vertical {}".format(username, site))
134+
connections.switch_user_and_site(rest_connection=t, username=username, site_content_url=site)
135+
tableau_tools_logger.log("Token is now {}".format(t.token))
136+
# Rerun the orginal callback command
137+
tableau_tools_logger.log("Doing callback function again now that new token exists")
138+
response = callback_function(t, **kwargs)
139+
# Originally, the code looked at the following two exceptions. This is been replaced by looking at NotSignedInException
140+
# RecoverableHTTPException is an exception from tableau_tools, when it is known what the error represents
141+
# HTTPError is a Requests library exception, which might happen if tableau_tools doesn't wrap the particular error.
142+
# except (RecoverableHTTPException, HTTPError) as e:
143+
# if e.http_code == 401:
144+
except Exception as e:
145+
raise e
146+
# Destroy REST API Connection Object, which is just used within this code block
147+
del t
148+
# Return Response
149+
return response
150+
151+
# There were originally separate functions but they shared enough code to be merged together
152+
def admin_request(request, site, callback_function, **kwargs):
153+
# We don't pass the 'request' here, because it would have the end user's username attached via the session
154+
# The point is that username ends up None in the generic_request call, forcing it to use the admin
155+
return generic_request(None, site, callback_function, **kwargs)
156+
157+
158+
#
159+
# Here is an example of an actual exposed endpoint
160+
#
161+
162+
# This is what is passed in as the callback function - so rest_connection is the 't' object passed in by generic_request
163+
# Returns all of the Projects a user can see content in, alphabetically sorted
164+
def query_projects(rest_connection: TableauServerRest32):
165+
p_sort = Sort('name', 'asc')
166+
p = rest_connection.query_projects_json(sorts=[p_sort, ])
167+
return JsonResponse(p)
168+
169+
# An exposed endpoint linked to an actual URL
170+
def projects(request, site):
171+
#log("Starting to request all workbooks")
172+
# Note we are just wrapping the generic request (this one doesn't take keyword arguments, but anything after
173+
# 'query_projects' would be passed as an argument into the query_projects function (if it took arguments)
174+
response = generic_request(request, site, query_projects)
175+
return response

examples/permissions_auditing.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
default.enable_logging(logger)
1212
default.signin()
1313

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

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

26-
project_caps = default.available_capabilities[default.api_version]['project']
26+
project_caps = Permissions.available_capabilities[default.api_version]['project']
2727
for cap in project_caps:
28-
headers.append(',{}'.format(cap))
29-
workbook_caps = default.available_capabilities[default.api_version]['workbook']
28+
headers.append(cap)
29+
workbook_caps = Permissions.available_capabilities[default.api_version]['workbook']
3030
for cap in workbook_caps:
31-
headers.append(',{}'.format(cap))
32-
datasource_caps = default.available_capabilities[default.api_version]['datasource']
31+
headers.append(cap)
32+
datasource_caps = Permissions.available_capabilities[default.api_version]['datasource']
3333
for cap in datasource_caps:
34-
headers.append(',{}'.format(cap))
34+
headers.append(cap)
3535
output_writer.writerow(headers)
3636

3737
for site_content_url in site_content_urls:
@@ -53,13 +53,13 @@
5353
all_perms_list = proj_obj.convert_all_permissions_to_list(all_perms[luid])
5454
if site_content_url is None:
5555
site_content_url = ''
56-
output_row.append(site_content_url.encode('utf-8'))
57-
output_row.append(project.encode('utf-8'))
58-
output_row.append(projects_dict[project].encode('utf-8'))
56+
output_row.append(site_content_url)
57+
output_row.append(project)
58+
output_row.append(projects_dict[project])
5959
output_row.append(str(proj_obj.are_permissions_locked()))
60-
output_row.append(all_perms[luid]["type"].encode('utf-8'))
61-
output_row.append(all_perms[luid]["name"].encode('utf-8'))
62-
output_row.append(luid.encode('utf-8'))
60+
output_row.append(all_perms[luid]["type"])
61+
output_row.append(all_perms[luid]["name"])
62+
output_row.append(luid)
6363
output_row.extend(all_perms_list)
6464
output_writer.writerow(output_row)
6565

examples/template_publish_sample.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def __init__(self, orig_content_url):
245245
temp_wb_file = t_file.save_new_file('Modified Workbook'.format(wb))
246246
new_workbook_luid = d.workbooks.publish_workbook(workbook_filename=temp_wb_file, workbook_name=wb,
247247
project_obj=dest_project,
248-
overwrite=True, check_published_ds=False)
248+
overwrite=True)
249249
print('Published new workbook {}'.format(new_workbook_luid))
250250
os.remove(temp_wb_file)
251251

0 commit comments

Comments
 (0)