diff --git a/samples/login.py b/samples/login.py new file mode 100644 index 000000000..aaa21ab25 --- /dev/null +++ b/samples/login.py @@ -0,0 +1,52 @@ +#### +# 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) + + # 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_with_personal_access_token(tableau_auth): + print('Logged in successfully') + + +if __name__ == '__main__': + main() 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..0bb9b2c02 --- /dev/null +++ b/tableauserverclient/models/personal_access_token_auth.py @@ -0,0 +1,11 @@ +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 + + @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 3b60741d6..cf04c1a97 100644 --- a/tableauserverclient/models/tableau_auth.py +++ b/tableauserverclient/models/tableau_auth.py @@ -25,3 +25,7 @@ def site(self, value): warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.', DeprecationWarning) self.site_id = value + + @property + 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..10f4cb4db 100644 --- a/tableauserverclient/server/endpoint/auth_endpoint.py +++ b/tableauserverclient/server/endpoint/auth_endpoint.py @@ -35,9 +35,14 @@ 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="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') diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 72bf90d80..6a3f811b6 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -47,11 +47,14 @@ 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 diff --git a/test/test_auth.py b/test/test_auth.py index 870064db0..28e241335 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')