Skip to content

Commit

Permalink
Updated tableau_base and added a new example file for showing how to …
Browse files Browse the repository at this point in the history
…easily use and keep track of impersonated sessions
  • Loading branch information
Bryant Howell committed Apr 23, 2019
1 parent 0258044 commit 0443f98
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 4 deletions.
221 changes: 221 additions & 0 deletions examples/use_admin_account_with_impersonation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-

from tableau_tools.tableau_rest_api import *
from tableau_tools import *

# This class is designed to take a single TableauRestApiConnectionNN object and then swaps around the sign-in tokens
# as necessary

class RestConnectionsManager():

def __init__(self, rest_connection_object):
"""
:type rest_connection_object: TableauRestApiConnection32
"""
# Each site should have a "master token" for signing in as the impersonated user the first time
self.site_master_tokens = {} # site : token
# This collection then holds any tokens from an individual user's session on a given site
self.site_user_tokens = {} # site : { username : token }

self.rest_connection = rest_connection_object # type: TableauRestApiConnection32

self.connection_signed_in = False


def _sign_in_error(self):
print("Tableau Server REST API Service unreachable")
self.connection_signed_in = False
return False

def _sign_in(self):
self.rest_connection.signin()
self.default_connection_token = self.rest_connection.token
return True

# This command can sign in from the very beginning if necessary
def sign_in_connection_object(self):
# This is a failure within this code, not a failure to reach the Tableau Server and sign in
if self.rest_connection is None:
raise NotSignedInException()

# Try to sign in to the Tableau Server REST API
try:
return self._sign_in()

# Trying all these exception types than capturing anything, but probably should figure exactly what is wrong
except NotSignedInException as e:
try:
return self._sign_in()
except:
return self._sign_in_error()
# Try to sign-in again?
except RecoverableHTTPException as e:
try:
return self._sign_in()
except:
return self._sign_in_error()
# Should be capturing requests ConnectionError exception
except Exception as e:
try:
return self._sign_in()
except:
return self._sign_in_error()


def sign_in_site_master(self, site_content_url):
self.rest_connection.token = None
self.rest_connection.site_content_url = site_content_url
try:
self.rest_connection.signin()
except:
try:
self.rest_connection.signin()
except:
raise

# Now grab that token
self.site_master_tokens[site_content_url] = self.rest_connection.token

# If no exist site
if site_content_url not in self.site_user_tokens:
self.site_user_tokens[site_content_url] = {}

# And reset back to the default
self.rest_connection.token = self.default_connection_token
return True

# All this check is if a user token exists
def check_user_token(self, username, site_content_url):
# Has the site been signed into before? If not, create it
if site_content_url not in self.site_master_tokens.keys():
# If the site has no master token, create it
# But we're keeping the same connection object, to limit the total number of tokens
self.sign_in_site_master(site_content_url)

# Also create an entry in the users dict for this vertical. The check is probably unnecessary but why not
if site_content_url not in self.site_user_tokens.keys():
self.site_user_tokens[site_content_url] = {}
# No user token can exist if nothing even existed on that site yet
return False
# Do they have an entry?
elif username in self.site_user_tokens[site_content_url].keys():
# Now check if a token exists
if self.site_user_tokens[site_content_url][username] is None:
return False
else:
return True
# Didn't find them, no connection
else:
return False


def create_user_connection(self, username, site_content_url):
# Swap to the master session for the vsite to get the user luid
self.rest_connection.token = self.site_master_tokens[site_content_url]
self.rest_connection.site_content_url = site_content_url
try:
user_luid = self.rest_connection.query_user_luid(username)

except:
# Retry at least once
try:
user_luid = self.rest_connection.query_user_luid(username)
except:
# Assume something wrong with the site_master token, create new session
try:
self.sign_in_site_master(site_content_url)
self.rest_connection.token = self.site_master_tokens[site_content_url]
user_luid = self.rest_connection.query_user_luid(username)
except:
# Maybe another check here?
raise


# Now blank the connection token so you can sign in
self.rest_connection.token = None
try:
self.rest_connection.signin(user_luid)
self.site_user_tokens[username] = self.rest_connection.token
# Should this be exception instead of return of True?
return True
# This is bad practice to capture any exception, improve when we know what a "user not found" looks like

except:
# Try one more time in case it is connection issue
try:
self.rest_connection.signin(user_luid)
self.site_user_tokens[username] = self.rest_connection.token
except:
# Should this be exception instead of return of True?
return False

def switch_user_and_site(self, username, site_content_url):
user_exists = self.check_user_token(username, site_content_url)
if user_exists is False:
# Create the connection
self.create_user_connection(username, site_content_url)
elif user_exists is True:
# This token could be out of date, but test for that exception when you try to run a command
self.rest_connection.token = self.site_user_tokens[site_content_url][username]
self.rest_connection.site_content_url = site_content_url
else:
raise Exception()

def switch_to_site_master(self, site_content_url):
if site_content_url not in self.site_master_tokens.keys():
self.sign_in_site_master(site_content_url)
self.rest_connection.token = self.site_master_tokens[site_content_url]
self.rest_connection.site_content_url = site_content_url


# Connect to the default site to bootstrap the process

server = ""
master_default_username = ""
master_default_password = ""
default_site_content_url = ""
d = TableauRestApiConnection32(server=server, username=master_default_username, password=master_default_password,
site_content_url=default_site_content_url)

# This manages all the connections here on out
connections = RestConnectionsManager(rest_connection_object=d)
connections.sign_in_connection_object()

# Examples of using the connection manager
connections.switch_user_and_site("some_user", "site_a")
my_projects = connections.rest_connection.query_projects()
my_projects_dict = connections.rest_connection.convert_xml_list_to_name_id_dict(my_projects)
print(my_projects_dict)

# Now switching to a different user
connections.switch_user_and_site("some_other_user", "site_b")
my_projects = connections.rest_connection.query_projects()
my_projects_dict = connections.rest_connection.convert_xml_list_to_name_id_dict(my_projects)
print(my_projects_dict)

#
# Simple implementation for single site
#

user_rest_connections = {}
master_username = 'site_admin'
master_password = 'hackm3'
site_content_url = "mysite"
m = TableauRestApiConnection32(server=server, username=master_username, password=master_password,
site_content_url=site_content_url)
m.signin()

user_to_impersonate_1 = 'user_a'
user_luid = m.query_user_luid(user_to_impersonate_1)
user_rest_connections[user_to_impersonate_1] = TableauRestApiConnection32(server=server, username=master_username,
password=master_password,
site_content_url=site_content_url)

user_rest_connections[user_to_impersonate_1].signin(user_luid)

user_to_impersonate_2 = 'user_b'
user_luid = m.query_user_luid(user_to_impersonate_2)
user_rest_connections[user_to_impersonate_2] = TableauRestApiConnection32(server=server, username=master_username,
password=master_password,
site_content_url=site_content_url)
user_rest_connections[user_to_impersonate_2].signin(user_luid)
8 changes: 4 additions & 4 deletions tableau_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def set_tableau_server_version(self, tableau_server_version):
"""
# API Versioning (starting in 9.2)
if unicode(tableau_server_version)in [u"9.2", u"9.3", u"10.0", u"10.1", u"10.2", u"10.3", u"10.4", u"10.5",
u'2018.1', u'2018.2', u'2018.3']:
u'2018.1', u'2018.2', u'2018.3', u'2019.1']:
if unicode(tableau_server_version) == u"9.2":
self.api_version = u"2.1"
elif unicode(tableau_server_version) == u"9.3":
Expand Down Expand Up @@ -444,13 +444,13 @@ def convert_view_content_url_to_embed_url(content_url):

# Generic method for XML lists for the "query" actions to name -> id dict
@staticmethod
def convert_xml_list_to_name_id_dict(lxml_obj):
def convert_xml_list_to_name_id_dict(etree_obj):
"""
:type lxml_obj: etree.Element
:type etree_obj: etree.Element
:return: dict
"""
d = {}
for element in lxml_obj:
for element in etree_obj:
e_id = element.get("id")
# If list is collection, have to run one deeper
if e_id is None:
Expand Down

0 comments on commit 0443f98

Please sign in to comment.