From 3180cd7f01fb21786e17e2ab7425a6c547ad0c11 Mon Sep 17 00:00:00 2001 From: Francisco Pagliaricci Date: Tue, 23 Jul 2019 00:38:22 +0000 Subject: [PATCH 1/7] Adding model for PersonalAccessToken authentication information --- tableauserverclient/__init__.py | 2 +- tableauserverclient/models/__init__.py | 1 + .../models/personal_access_token_auth.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tableauserverclient/models/personal_access_token_auth.py diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 85972d48b..3494e5f1f 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -1,7 +1,7 @@ from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\ GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \ - SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ + SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \ SubscriptionItem, Target from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \ diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index 63a861cbb..872909adb 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -11,6 +11,7 @@ from .server_info_item import ServerInfoItem from .site_item import SiteItem from .tableau_auth import TableauAuth +from .personal_access_token_auth import PersonalAccessTokenAuth from .target import Target from .task_item import TaskItem from .user_item import UserItem diff --git a/tableauserverclient/models/personal_access_token_auth.py b/tableauserverclient/models/personal_access_token_auth.py new file mode 100644 index 000000000..9ccdb93f1 --- /dev/null +++ b/tableauserverclient/models/personal_access_token_auth.py @@ -0,0 +1,10 @@ +class PersonalAccessTokenAuth(object): + def __init__(self, token_name, personal_access_token, site_id=''): + self.token_name = token_name + self.personal_access_token = personal_access_token + self.site_id = site_id + # Personal Access Tokens doesn't support impersonation. + self.user_id_to_impersonate = None + + def credentials(self): + return { 'clientId': self.token_name, 'personalAccessToken': self.personal_access_token } From 52036108bac9d92b89f0fc7793c82da5628179e6 Mon Sep 17 00:00:00 2001 From: Francisco Pagliaricci Date: Tue, 23 Jul 2019 00:38:56 +0000 Subject: [PATCH 2/7] Making the signin request accept generic credentials --- tableauserverclient/models/tableau_auth.py | 3 +++ tableauserverclient/server/endpoint/auth_endpoint.py | 2 +- tableauserverclient/server/request_factory.py | 8 +++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py index 3b60741d6..dfbf35c22 100644 --- a/tableauserverclient/models/tableau_auth.py +++ b/tableauserverclient/models/tableau_auth.py @@ -25,3 +25,6 @@ def site(self, value): warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.', DeprecationWarning) self.site_id = value + + def credentials(self): + return { 'name': self.username, 'password': self.password } diff --git a/tableauserverclient/server/endpoint/auth_endpoint.py b/tableauserverclient/server/endpoint/auth_endpoint.py index 84938ba63..aaa37e428 100644 --- a/tableauserverclient/server/endpoint/auth_endpoint.py +++ b/tableauserverclient/server/endpoint/auth_endpoint.py @@ -35,7 +35,7 @@ def sign_in(self, auth_req): user_id = parsed_response.find('.//t:user', namespaces=self.parent_srv.namespace).get('id', None) auth_token = parsed_response.find('t:credentials', namespaces=self.parent_srv.namespace).get('token', None) self.parent_srv._set_auth(site_id, user_id, auth_token) - logger.info('Signed into {0} as {1}'.format(self.parent_srv.server_address, auth_req.username)) + logger.info('Signed into {0} as user with id {1}'.format(self.parent_srv.server_address, user_id)) return Auth.contextmgr(self.sign_out) @api(version="2.0") diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 72bf90d80..fc2552f44 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -47,17 +47,19 @@ def _add_credentials_element(parent_element, connection_credentials): class AuthRequest(object): def signin_req(self, auth_item): xml_request = ET.Element('tsRequest') + credentials_element = ET.SubElement(xml_request, 'credentials') - credentials_element.attrib['name'] = auth_item.username - credentials_element.attrib['password'] = auth_item.password + for attribute_name, attribute_value in auth_item.credentials().items(): + credentials_element.attrib[attribute_name] = attribute_value + site_element = ET.SubElement(credentials_element, 'site') site_element.attrib['contentUrl'] = auth_item.site_id + if auth_item.user_id_to_impersonate: user_element = ET.SubElement(credentials_element, 'user') user_element.attrib['id'] = auth_item.user_id_to_impersonate return ET.tostring(xml_request) - class DatasourceRequest(object): def _generate_xml(self, datasource_item, connection_credentials=None, connections=None): xml_request = ET.Element('tsRequest') From 0e24bb089853ce90765bfe6c244a3e2e13550517 Mon Sep 17 00:00:00 2001 From: Francisco Pagliaricci Date: Tue, 23 Jul 2019 00:39:32 +0000 Subject: [PATCH 3/7] Adding login sample --- samples/login.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 samples/login.py diff --git a/samples/login.py b/samples/login.py new file mode 100644 index 000000000..033f60d63 --- /dev/null +++ b/samples/login.py @@ -0,0 +1,48 @@ +#### +# This script demonstrates how to log in to Tableau Server Client. +# +# To run the script, you must have installed Python 2.7.9 or later. +#### + +import argparse +import getpass +import logging + +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description='Logs in to the server.') + + parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', + help='desired logging level (set to error by default)') + + parser.add_argument('--server', '-s', required=True, help='server address') + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--username', '-u', help='username to sign into the server') + group.add_argument('--token-name', '-n', help='name of the personal access token used to sign into the server') + + args = parser.parse_args() + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + server = TSC.Server(args.server) + + if args.username: + # Trying to authenticate using username and password. + password = getpass.getpass("Password: ") + tableau_auth = TSC.TableauAuth(args.username, password) + else: + # Trying to authenticate using personal access tokens. + personal_access_token = getpass.getpass("Personal Access Token: ") + tableau_auth = TSC.PersonalAccessTokenAuth(token_name=args.token_name, personal_access_token=personal_access_token) + + with server.auth.sign_in(tableau_auth): + print('Logged in successfully') + + +if __name__ == '__main__': + main() \ No newline at end of file From 9b64846b706f6743a063cd22d0cf30f1752070bf Mon Sep 17 00:00:00 2001 From: Francisco Pagliaricci Date: Tue, 23 Jul 2019 00:55:42 +0000 Subject: [PATCH 4/7] adding sign in tests for tokens --- test/test_auth.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/test_auth.py b/test/test_auth.py index 870064db0..14ac47a43 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -27,6 +27,19 @@ def test_sign_in(self): self.assertEqual('6b7179ba-b82b-4f0f-91ed-812074ac5da6', self.server.site_id) self.assertEqual('1a96d216-e9b8-497b-a82a-0b899a965e01', self.server.user_id) + def test_sign_in_with_personal_access_tokens(self): + with open(SIGN_IN_XML, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.post(self.baseurl + '/signin', text=response_xml) + tableau_auth = TSC.PersonalAccessTokenAuth(token_name='mytoken', + personal_access_token='Random123Generated', site_id='Samples') + self.server.auth.sign_in(tableau_auth) + + self.assertEqual('eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l', self.server.auth_token) + self.assertEqual('6b7179ba-b82b-4f0f-91ed-812074ac5da6', self.server.site_id) + self.assertEqual('1a96d216-e9b8-497b-a82a-0b899a965e01', self.server.user_id) + def test_sign_in_impersonate(self): with open(SIGN_IN_IMPERSONATE_XML, 'rb') as f: response_xml = f.read().decode('utf-8') @@ -48,6 +61,14 @@ def test_sign_in_error(self): tableau_auth = TSC.TableauAuth('testuser', 'wrongpassword') self.assertRaises(TSC.ServerResponseError, self.server.auth.sign_in, tableau_auth) + def test_sign_in_invalid_token(self): + with open(SIGN_IN_ERROR_XML, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.post(self.baseurl + '/signin', text=response_xml, status_code=401) + tableau_auth = TSC.PersonalAccessTokenAuth(token_name='mytoken', personal_access_token='invalid') + self.assertRaises(TSC.ServerResponseError, self.server.auth.sign_in, tableau_auth) + def test_sign_in_without_auth(self): with open(SIGN_IN_ERROR_XML, 'rb') as f: response_xml = f.read().decode('utf-8') From 887417b3f7e70cf813a94ead8705ee24836d22b6 Mon Sep 17 00:00:00 2001 From: Francisco Pagliaricci Date: Tue, 23 Jul 2019 04:44:33 +0000 Subject: [PATCH 5/7] Fixing lint issues --- samples/login.py | 47 ++++++++++--------- .../models/personal_access_token_auth.py | 2 +- tableauserverclient/models/tableau_auth.py | 2 +- tableauserverclient/server/request_factory.py | 1 + test/test_auth.py | 4 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/samples/login.py b/samples/login.py index 033f60d63..db09cec19 100644 --- a/samples/login.py +++ b/samples/login.py @@ -12,37 +12,38 @@ def main(): - parser = argparse.ArgumentParser(description='Logs in to the server.') + parser = argparse.ArgumentParser(description='Logs in to the server.') - parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', - help='desired logging level (set to error by default)') + parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', + help='desired logging level (set to error by default)') - parser.add_argument('--server', '-s', required=True, help='server address') + parser.add_argument('--server', '-s', required=True, help='server address') - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('--username', '-u', help='username to sign into the server') - group.add_argument('--token-name', '-n', help='name of the personal access token used to sign into the server') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--username', '-u', help='username to sign into the server') + group.add_argument('--token-name', '-n', help='name of the personal access token used to sign into the server') - args = parser.parse_args() + args = parser.parse_args() - # Set logging level based on user input, or error by default - logging_level = getattr(logging, args.logging_level.upper()) - logging.basicConfig(level=logging_level) + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) - server = TSC.Server(args.server) + server = TSC.Server(args.server) - if args.username: - # Trying to authenticate using username and password. - password = getpass.getpass("Password: ") - tableau_auth = TSC.TableauAuth(args.username, password) - else: - # Trying to authenticate using personal access tokens. - personal_access_token = getpass.getpass("Personal Access Token: ") - tableau_auth = TSC.PersonalAccessTokenAuth(token_name=args.token_name, personal_access_token=personal_access_token) + if args.username: + # Trying to authenticate using username and password. + password = getpass.getpass("Password: ") + tableau_auth = TSC.TableauAuth(args.username, password) + else: + # Trying to authenticate using personal access tokens. + personal_access_token = getpass.getpass("Personal Access Token: ") + tableau_auth = TSC.PersonalAccessTokenAuth(token_name=args.token_name, + personal_access_token=personal_access_token) - with server.auth.sign_in(tableau_auth): - print('Logged in successfully') + with server.auth.sign_in(tableau_auth): + print('Logged in successfully') if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/tableauserverclient/models/personal_access_token_auth.py b/tableauserverclient/models/personal_access_token_auth.py index 9ccdb93f1..aaf4aa97e 100644 --- a/tableauserverclient/models/personal_access_token_auth.py +++ b/tableauserverclient/models/personal_access_token_auth.py @@ -7,4 +7,4 @@ def __init__(self, token_name, personal_access_token, site_id=''): self.user_id_to_impersonate = None def credentials(self): - return { 'clientId': self.token_name, 'personalAccessToken': self.personal_access_token } + return {'clientId': self.token_name, 'personalAccessToken': self.personal_access_token} diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py index dfbf35c22..f63013f9e 100644 --- a/tableauserverclient/models/tableau_auth.py +++ b/tableauserverclient/models/tableau_auth.py @@ -27,4 +27,4 @@ def site(self, value): self.site_id = value def credentials(self): - return { 'name': self.username, 'password': self.password } + return {'name': self.username, 'password': self.password} diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index fc2552f44..1f1b2d4a7 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -60,6 +60,7 @@ def signin_req(self, auth_item): user_element.attrib['id'] = auth_item.user_id_to_impersonate return ET.tostring(xml_request) + class DatasourceRequest(object): def _generate_xml(self, datasource_item, connection_credentials=None, connections=None): xml_request = ET.Element('tsRequest') diff --git a/test/test_auth.py b/test/test_auth.py index 14ac47a43..28e241335 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -39,7 +39,7 @@ def test_sign_in_with_personal_access_tokens(self): self.assertEqual('eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l', self.server.auth_token) self.assertEqual('6b7179ba-b82b-4f0f-91ed-812074ac5da6', self.server.site_id) self.assertEqual('1a96d216-e9b8-497b-a82a-0b899a965e01', self.server.user_id) - + def test_sign_in_impersonate(self): with open(SIGN_IN_IMPERSONATE_XML, 'rb') as f: response_xml = f.read().decode('utf-8') @@ -68,7 +68,7 @@ def test_sign_in_invalid_token(self): m.post(self.baseurl + '/signin', text=response_xml, status_code=401) tableau_auth = TSC.PersonalAccessTokenAuth(token_name='mytoken', personal_access_token='invalid') self.assertRaises(TSC.ServerResponseError, self.server.auth.sign_in, tableau_auth) - + def test_sign_in_without_auth(self): with open(SIGN_IN_ERROR_XML, 'rb') as f: response_xml = f.read().decode('utf-8') From e906c4ce9f871cc5643a5be6655d582826029659 Mon Sep 17 00:00:00 2001 From: Francisco Pagliaricci Date: Wed, 24 Jul 2019 01:23:21 +0000 Subject: [PATCH 6/7] moving auth credentials to a property --- tableauserverclient/models/personal_access_token_auth.py | 1 + tableauserverclient/models/tableau_auth.py | 1 + tableauserverclient/server/request_factory.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tableauserverclient/models/personal_access_token_auth.py b/tableauserverclient/models/personal_access_token_auth.py index aaf4aa97e..0bb9b2c02 100644 --- a/tableauserverclient/models/personal_access_token_auth.py +++ b/tableauserverclient/models/personal_access_token_auth.py @@ -6,5 +6,6 @@ def __init__(self, token_name, personal_access_token, site_id=''): # Personal Access Tokens doesn't support impersonation. self.user_id_to_impersonate = None + @property def credentials(self): return {'clientId': self.token_name, 'personalAccessToken': self.personal_access_token} diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py index f63013f9e..cf04c1a97 100644 --- a/tableauserverclient/models/tableau_auth.py +++ b/tableauserverclient/models/tableau_auth.py @@ -26,5 +26,6 @@ def site(self, value): DeprecationWarning) self.site_id = value + @property def credentials(self): return {'name': self.username, 'password': self.password} diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 1f1b2d4a7..6a3f811b6 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -49,7 +49,7 @@ def signin_req(self, auth_item): xml_request = ET.Element('tsRequest') credentials_element = ET.SubElement(xml_request, 'credentials') - for attribute_name, attribute_value in auth_item.credentials().items(): + for attribute_name, attribute_value in auth_item.credentials.items(): credentials_element.attrib[attribute_name] = attribute_value site_element = ET.SubElement(credentials_element, 'site') From 784d35ff64ee81976bc25ab2c3dfed2db5e7dcc1 Mon Sep 17 00:00:00 2001 From: Francisco Pagliaricci Date: Fri, 26 Jul 2019 05:09:47 +0000 Subject: [PATCH 7/7] Making PAT use a specific sign in method, to avoid misuing the feature attempting to use it against older versions of the server --- samples/login.py | 13 ++++++++----- .../server/endpoint/auth_endpoint.py | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/samples/login.py b/samples/login.py index db09cec19..aaa21ab25 100644 --- a/samples/login.py +++ b/samples/login.py @@ -25,24 +25,27 @@ def main(): args = parser.parse_args() - # Set logging level based on user input, or error by default + # Set logging level based on user input, or error by default. logging_level = getattr(logging, args.logging_level.upper()) logging.basicConfig(level=logging_level) - server = TSC.Server(args.server) + # Make sure we use an updated version of the rest apis. + server = TSC.Server(args.server, use_server_version=True) if args.username: # Trying to authenticate using username and password. password = getpass.getpass("Password: ") tableau_auth = TSC.TableauAuth(args.username, password) + with server.auth.sign_in(tableau_auth): + print('Logged in successfully') + else: # Trying to authenticate using personal access tokens. personal_access_token = getpass.getpass("Personal Access Token: ") tableau_auth = TSC.PersonalAccessTokenAuth(token_name=args.token_name, personal_access_token=personal_access_token) - - with server.auth.sign_in(tableau_auth): - print('Logged in successfully') + with server.auth.sign_in_with_personal_access_token(tableau_auth): + print('Logged in successfully') if __name__ == '__main__': diff --git a/tableauserverclient/server/endpoint/auth_endpoint.py b/tableauserverclient/server/endpoint/auth_endpoint.py index aaa37e428..10f4cb4db 100644 --- a/tableauserverclient/server/endpoint/auth_endpoint.py +++ b/tableauserverclient/server/endpoint/auth_endpoint.py @@ -38,6 +38,11 @@ def sign_in(self, auth_req): logger.info('Signed into {0} as user with id {1}'.format(self.parent_srv.server_address, user_id)) return Auth.contextmgr(self.sign_out) + @api(version="3.6") + def sign_in_with_personal_access_token(self, auth_req): + # We use the same request that username/password login uses. + return self.sign_in(auth_req) + @api(version="2.0") def sign_out(self): url = "{0}/{1}".format(self.baseurl, 'signout')