diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py
index 3494e5f1f..d1b8a4e74 100644
--- a/tableauserverclient/__init__.py
+++ b/tableauserverclient/__init__.py
@@ -3,7 +3,7 @@
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \
SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
- SubscriptionItem, Target
+ SubscriptionItem, Target, PermissionsRule, Permission
from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
from ._version import get_versions
diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py
index 872909adb..f96f78565 100644
--- a/tableauserverclient/models/__init__.py
+++ b/tableauserverclient/models/__init__.py
@@ -18,3 +18,4 @@
from .view_item import ViewItem
from .workbook_item import WorkbookItem
from .subscription_item import SubscriptionItem
+from .permissions_item import PermissionsRule, Permission
diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py
index b00e6cbea..e76a42aae 100644
--- a/tableauserverclient/models/datasource_item.py
+++ b/tableauserverclient/models/datasource_item.py
@@ -23,6 +23,8 @@ def __init__(self, project_id, name=None):
self.project_id = project_id
self.tags = set()
+ self._permissions = None
+
@property
def connections(self):
if self._connections is None:
@@ -30,6 +32,13 @@ def connections(self):
raise UnpopulatedPropertyError(error)
return self._connections()
+ @property
+ def permissions(self):
+ if self._permissions is None:
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
+ return self._permissions()
+
@property
def content_url(self):
return self._content_url
@@ -84,6 +93,9 @@ def updated_at(self):
def _set_connections(self, connections):
self._connections = connections
+ def _set_permissions(self, permissions):
+ self._permissions = permissions
+
def _parse_common_elements(self, datasource_xml, ns):
if not isinstance(datasource_xml, ET.Element):
datasource_xml = ET.fromstring(datasource_xml).find('.//t:datasource', namespaces=ns)
diff --git a/tableauserverclient/models/exceptions.py b/tableauserverclient/models/exceptions.py
index 28d738e73..86c28ac33 100644
--- a/tableauserverclient/models/exceptions.py
+++ b/tableauserverclient/models/exceptions.py
@@ -1,2 +1,6 @@
class UnpopulatedPropertyError(Exception):
pass
+
+
+class UnknownGranteeTypeError(Exception):
+ pass
diff --git a/tableauserverclient/models/group_item.py b/tableauserverclient/models/group_item.py
index 0efdfa6ea..d37769006 100644
--- a/tableauserverclient/models/group_item.py
+++ b/tableauserverclient/models/group_item.py
@@ -1,10 +1,14 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_not_empty
+from .reference_item import ResourceReference
class GroupItem(object):
- def __init__(self, name):
+
+ tag_name = 'group'
+
+ def __init__(self, name=None):
self._domain_name = None
self._id = None
self._users = None
@@ -35,6 +39,9 @@ def users(self):
# Each call to `.users` should create a new pager, this just runs the callable
return self._users()
+ def to_reference(self):
+ return ResourceReference(id_=self.id, tag_name=self.tag_name)
+
def _set_users(self, users):
self._users = users
@@ -53,3 +60,7 @@ def from_response(cls, resp, ns):
group_item._domain_name = domain_elem.get('name', None)
all_group_items.append(group_item)
return all_group_items
+
+ @staticmethod
+ def as_reference(id_):
+ return ResourceReference(id_, GroupItem.tag_name)
diff --git a/tableauserverclient/models/permissions_item.py b/tableauserverclient/models/permissions_item.py
new file mode 100644
index 000000000..2c2abdf82
--- /dev/null
+++ b/tableauserverclient/models/permissions_item.py
@@ -0,0 +1,93 @@
+import xml.etree.ElementTree as ET
+import logging
+
+from .exceptions import UnknownGranteeTypeError
+from .user_item import UserItem
+from .group_item import GroupItem
+
+logger = logging.getLogger('tableau.models.permissions_item')
+
+
+class Permission:
+
+ class Mode:
+ Allow = 'Allow'
+ Deny = 'Deny'
+
+ class Capability:
+ AddComment = 'AddComment'
+ ChangeHierarchy = 'ChangeHierarchy'
+ ChangePermissions = 'ChangePermissions'
+ Connect = 'Connect'
+ Delete = 'Delete'
+ ExportData = 'ExportData'
+ ExportImage = 'ExportImage'
+ ExportXml = 'ExportXml'
+ Filter = 'Filter'
+ ProjectLeader = 'ProjectLeader'
+ Read = 'Read'
+ ShareView = 'ShareView'
+ ViewComments = 'ViewComments'
+ ViewUnderlyingData = 'ViewUnderlyingData'
+ WebAuthoring = 'WebAuthoring'
+ Write = 'Write'
+
+ class Resource:
+ Workbook = 'workbook'
+ Datasource = 'datasource'
+ Flow = 'flow'
+
+
+class PermissionsRule(object):
+
+ def __init__(self, grantee, capabilities):
+ self.grantee = grantee
+ self.capabilities = capabilities
+
+ @classmethod
+ def from_response(cls, resp, ns=None):
+ parsed_response = ET.fromstring(resp)
+
+ rules = []
+ permissions_rules_list_xml = parsed_response.findall('.//t:granteeCapabilities',
+ namespaces=ns)
+
+ for grantee_capability_xml in permissions_rules_list_xml:
+ capability_dict = {}
+
+ grantee = PermissionsRule._parse_grantee_element(grantee_capability_xml, ns)
+
+ for capability_xml in grantee_capability_xml.findall(
+ './/t:capabilities/t:capability', namespaces=ns):
+ name = capability_xml.get('name')
+ mode = capability_xml.get('mode')
+
+ capability_dict[name] = mode
+
+ rule = PermissionsRule(grantee,
+ capability_dict)
+ rules.append(rule)
+
+ return rules
+
+ @staticmethod
+ def _parse_grantee_element(grantee_capability_xml, ns):
+ """Use Xpath magic and some string splitting to get the right object type from the xml"""
+
+ # Get the first element in the tree with an 'id' attribute
+ grantee_element = grantee_capability_xml.findall('.//*[@id]', namespaces=ns).pop()
+ grantee_id = grantee_element.get('id', None)
+ grantee_type = grantee_element.tag.split('}').pop()
+
+ if grantee_id is None:
+ logger.error('Cannot find grantee type in response')
+ raise UnknownGranteeTypeError()
+
+ if grantee_type == 'user':
+ grantee = UserItem.as_reference(grantee_id)
+ elif grantee_type == 'group':
+ grantee = GroupItem.as_reference(grantee_id)
+ else:
+ raise UnknownGranteeTypeError("No support for grantee type of {}".format(grantee_type))
+
+ return grantee
diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py
index 92e0282ae..15223e695 100644
--- a/tableauserverclient/models/project_item.py
+++ b/tableauserverclient/models/project_item.py
@@ -1,5 +1,9 @@
import xml.etree.ElementTree as ET
+
+from .permissions_item import Permission
+
from .property_decorators import property_is_enum, property_not_empty
+from .exceptions import UnpopulatedPropertyError
class ProjectItem(object):
@@ -15,10 +19,43 @@ def __init__(self, name, description=None, content_permissions=None, parent_id=N
self.content_permissions = content_permissions
self.parent_id = parent_id
+ self._permissions = None
+ self._default_workbook_permissions = None
+ self._default_datasource_permissions = None
+ self._default_flow_permissions = None
+
@property
def content_permissions(self):
return self._content_permissions
+ @property
+ def permissions(self):
+ if self._permissions is None:
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
+ return self._permissions()
+
+ @property
+ def default_datasource_permissions(self):
+ if self._default_datasource_permissions is None:
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
+ return self._default_datasource_permissions()
+
+ @property
+ def default_workbook_permissions(self):
+ if self._default_workbook_permissions is None:
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
+ return self._default_workbook_permissions()
+
+ @property
+ def default_flow_permissions(self):
+ if self._default_flow_permissions is None:
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
+ return self._default_flow_permissions()
+
@content_permissions.setter
@property_is_enum(ContentPermissions)
def content_permissions(self, value):
@@ -61,6 +98,12 @@ def _set_values(self, project_id, name, description, content_permissions, parent
if parent_id:
self.parent_id = parent_id
+ def _set_permissions(self, permissions):
+ self._permissions = permissions
+
+ def _set_default_permissions(self, permissions, content_type):
+ setattr(self, "_default_{content}_permissions".format(content=content_type), permissions)
+
@classmethod
def from_response(cls, resp, ns):
all_project_items = list()
diff --git a/tableauserverclient/models/reference_item.py b/tableauserverclient/models/reference_item.py
new file mode 100644
index 000000000..2cf0f0119
--- /dev/null
+++ b/tableauserverclient/models/reference_item.py
@@ -0,0 +1,21 @@
+class ResourceReference(object):
+
+ def __init__(self, id_, tag_name):
+ self.id = id_
+ self.tag_name = tag_name
+
+ @property
+ def id(self):
+ return self._id
+
+ @id.setter
+ def id(self, value):
+ self._id = value
+
+ @property
+ def tag_name(self):
+ return self._tag_name
+
+ @tag_name.setter
+ def tag_name(self, value):
+ self._tag_name = value
diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py
index 48e942ece..10ca7527d 100644
--- a/tableauserverclient/models/user_item.py
+++ b/tableauserverclient/models/user_item.py
@@ -2,9 +2,13 @@
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_is_enum, property_not_empty, property_not_nullable
from ..datetime_helpers import parse_datetime
+from .reference_item import ResourceReference
class UserItem(object):
+
+ tag_name = 'user'
+
class Roles:
Interactor = 'Interactor'
Publisher = 'Publisher'
@@ -30,7 +34,7 @@ class Auth:
SAML = 'SAML'
ServerDefault = 'ServerDefault'
- def __init__(self, name, site_role, auth_setting=None):
+ def __init__(self, name=None, site_role=None, auth_setting=None):
self._auth_setting = None
self._domain_name = None
self._external_auth_user_id = None
@@ -94,6 +98,9 @@ def workbooks(self):
raise UnpopulatedPropertyError(error)
return self._workbooks()
+ def to_reference(self):
+ return ResourceReference(id_=self.id, tag_name=self.tag_name)
+
def _set_workbooks(self, workbooks):
self._workbooks = workbooks
@@ -140,6 +147,10 @@ def from_response(cls, resp, ns):
all_user_items.append(user_item)
return all_user_items
+ @staticmethod
+ def as_reference(id_):
+ return ResourceReference(id_, UserItem.tag_name)
+
@staticmethod
def _parse_element(user_xml, ns):
id = user_xml.get('id', None)
diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py
index 8df036516..d518f23a4 100644
--- a/tableauserverclient/models/workbook_item.py
+++ b/tableauserverclient/models/workbook_item.py
@@ -3,6 +3,7 @@
from .property_decorators import property_not_nullable, property_is_boolean, property_is_materialized_views_config
from .tag_item import TagItem
from .view_item import ViewItem
+from .permissions_item import PermissionsRule
from ..datetime_helpers import parse_datetime
import copy
@@ -27,6 +28,7 @@ def __init__(self, project_id, name=None, show_tabs=False):
self.tags = set()
self.materialized_views_config = {'materialized_views_enabled': None,
'run_materialization_now': None}
+ self._permissions = None
@property
def connections(self):
@@ -35,6 +37,13 @@ def connections(self):
raise UnpopulatedPropertyError(error)
return self._connections()
+ @property
+ def permissions(self):
+ if self._permissions is None:
+ error = "Workbook item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
+ return self._permissions()
+
@property
def content_url(self):
return self._content_url
@@ -120,6 +129,9 @@ def materialized_views_config(self, value):
def _set_connections(self, connections):
self._connections = connections
+ def _set_permissions(self, permissions):
+ self._permissions = permissions
+
def _set_views(self, views):
self._views = views
diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py
index 8c5cb314c..7fa59ef3c 100644
--- a/tableauserverclient/server/__init__.py
+++ b/tableauserverclient/server/__init__.py
@@ -4,7 +4,7 @@
from .sort import Sort
from .. import ConnectionItem, DatasourceItem, JobItem, BackgroundJobItem, \
GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\
- UserItem, ViewItem, WorkbookItem, TaskItem, SubscriptionItem
+ UserItem, ViewItem, WorkbookItem, TaskItem, SubscriptionItem, PermissionsRule, Permission
from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \
Sites, Users, Views, Workbooks, Subscriptions, ServerResponseError, \
MissingRequiredFieldError
diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py
index 4d7a20b70..c46a7dc74 100644
--- a/tableauserverclient/server/endpoint/datasources_endpoint.py
+++ b/tableauserverclient/server/endpoint/datasources_endpoint.py
@@ -1,5 +1,8 @@
from .endpoint import Endpoint, api, parameter_added_in
from .exceptions import InternalServerError, MissingRequiredFieldError
+from .endpoint import api, parameter_added_in, Endpoint
+from .permissions_endpoint import _PermissionsEndpoint
+from .exceptions import MissingRequiredFieldError
from .fileuploads_endpoint import Fileuploads
from .resource_tagger import _ResourceTagger
from .. import RequestFactory, DatasourceItem, PaginationItem, ConnectionItem
@@ -24,6 +27,7 @@ class Datasources(Endpoint):
def __init__(self, parent_srv):
super(Datasources, self).__init__(parent_srv)
self._resource_tagger = _ResourceTagger(parent_srv)
+ self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
@property
def baseurl(self):
@@ -213,3 +217,19 @@ def publish(self, datasource_item, file_path, mode, connection_credentials=None,
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
return new_datasource
+ server_response = self.post_request(url, xml_request, content_type)
+ new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
+ logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
+ return new_datasource
+
+ @api(version='2.0')
+ def populate_permissions(self, item):
+ self._permissions.populate(item)
+
+ @api(version='2.0')
+ def update_permission(self, item, permission_item):
+ self._permissions.update(item, permission_item)
+
+ @api(version='2.0')
+ def delete_permission(self, item, capability_item):
+ self._permissions.delete(item, capability_item)
diff --git a/tableauserverclient/server/endpoint/default_permissions_endpoint.py b/tableauserverclient/server/endpoint/default_permissions_endpoint.py
new file mode 100644
index 000000000..f2e48db7a
--- /dev/null
+++ b/tableauserverclient/server/endpoint/default_permissions_endpoint.py
@@ -0,0 +1,79 @@
+import logging
+
+from .. import RequestFactory
+from ...models import PermissionsRule
+
+from .endpoint import Endpoint, api
+from .exceptions import MissingRequiredFieldError
+
+
+logger = logging.getLogger(__name__)
+
+
+class _DefaultPermissionsEndpoint(Endpoint):
+ ''' Adds default-permission model to another endpoint
+
+ Tableau default-permissions model applies only to databases and projects
+ and then takes an object type in the uri to set the defaults.
+ This class is meant to be instantated inside a parent endpoint which
+ has these supported endpoints
+ '''
+ def __init__(self, parent_srv, owner_baseurl):
+ super(_DefaultPermissionsEndpoint, self).__init__(parent_srv)
+
+ # owner_baseurl is the baseurl of the parent. The MUST be a lambda
+ # since we don't know the full site URL until we sign in. If
+ # populated without, we will get a sign-in error
+ self.owner_baseurl = owner_baseurl
+
+ def update_default_permissions(self, resource, permissions, content_type):
+ url = '{0}/{1}/default-permissions/{2}'.format(self.owner_baseurl(), resource.id, content_type)
+ update_req = RequestFactory.Permission.add_req(permissions)
+ response = self.put_request(url, update_req)
+ permissions = PermissionsRule.from_response(response.content,
+ self.parent_srv.namespace)
+ logger.info('Updated permissions for resource {0}'.format(resource.id))
+
+ return permissions
+
+ def delete_default_permission(self, resource, rule, content_type):
+ for capability, mode in rule.capabilities.items():
+ # Made readibility better but line is too long, will make this look better
+ url = '{baseurl}/{content_id}/default-permissions/\
+ {content_type}/{grantee_type}/{grantee_id}/{cap}/{mode}'.format(
+ baseurl=self.owner_baseurl(),
+ content_id=resource.id,
+ content_type=content_type,
+ grantee_type=rule.grantee.tag_name + 's',
+ grantee_id=rule.grantee.id,
+ cap=capability,
+ mode=mode)
+
+ logger.debug('Removing {0} permission for capabilty {1}'.format(
+ mode, capability))
+
+ self.delete_request(url)
+
+ logger.info('Deleted permission for {0} {1} item {2}'.format(
+ rule.grantee.tag_name,
+ rule.grantee.id,
+ resource.id))
+
+ def populate_default_permissions(self, item, content_type):
+ if not item.id:
+ error = "Server item is missing ID. Item must be retrieved from server first."
+ raise MissingRequiredFieldError(error)
+
+ def permission_fetcher():
+ return self._get_default_permissions(item, content_type)
+
+ item._set_default_permissions(permission_fetcher, content_type)
+ logger.info('Populated {0} permissions for item (ID: {1})'.format(item.id, content_type))
+
+ def _get_default_permissions(self, item, content_type, req_options=None):
+ url = "{0}/{1}/default-permissions/{2}".format(self.owner_baseurl(), item.id, content_type + "s")
+ server_response = self.get_request(url, req_options)
+ permissions = PermissionsRule.from_response(server_response.content,
+ self.parent_srv.namespace)
+
+ return permissions
diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py
index f16c9f8df..8c7e93607 100644
--- a/tableauserverclient/server/endpoint/endpoint.py
+++ b/tableauserverclient/server/endpoint/endpoint.py
@@ -1,5 +1,6 @@
from .exceptions import ServerResponseError, InternalServerError
from functools import wraps
+from xml.etree.ElementTree import ParseError
import logging
@@ -65,7 +66,11 @@ def _check_status(self, server_response):
if server_response.status_code >= 500:
raise InternalServerError(server_response)
elif server_response.status_code not in Success_codes:
- raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace)
+ try:
+ raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace)
+ except ParseError:
+ # not an xml error
+ raise NonXMLResponseError(server_response.content)
def get_unauthenticated_request(self, url, request_object=None):
return self._make_request(self.parent_srv.session.get, url, request_object=request_object)
diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py
index 0648d8814..757ca5552 100644
--- a/tableauserverclient/server/endpoint/exceptions.py
+++ b/tableauserverclient/server/endpoint/exceptions.py
@@ -46,6 +46,10 @@ class ItemTypeNotAllowed(Exception):
pass
+class NonXMLResponseError(Exception):
+ pass
+
+
class GraphQLError(Exception):
def __init__(self, error_payload):
self.error = error_payload
diff --git a/tableauserverclient/server/endpoint/permissions_endpoint.py b/tableauserverclient/server/endpoint/permissions_endpoint.py
new file mode 100644
index 000000000..6405f96a0
--- /dev/null
+++ b/tableauserverclient/server/endpoint/permissions_endpoint.py
@@ -0,0 +1,83 @@
+import logging
+
+from .. import RequestFactory, PermissionsRule
+
+from .endpoint import Endpoint, api
+from .exceptions import MissingRequiredFieldError
+
+
+logger = logging.getLogger(__name__)
+
+
+class _PermissionsEndpoint(Endpoint):
+ ''' Adds permission model to another endpoint
+
+ Tableau permissions model is identical between objects but they are nested under
+ the parent object endpoint (i.e. permissions for workbooks are under
+ /workbooks/:id/permission). This class is meant to be instantated inside a
+ parent endpoint which has these supported endpoints
+ '''
+ def __init__(self, parent_srv, owner_baseurl):
+ super(_PermissionsEndpoint, self).__init__(parent_srv)
+
+ # owner_baseurl is the baseurl of the parent. The MUST be a lambda
+ # since we don't know the full site URL until we sign in. If
+ # populated without, we will get a sign-in error
+ self.owner_baseurl = owner_baseurl
+
+ def update(self, resource, permissions):
+ url = '{0}/{1}/permissions'.format(self.owner_baseurl(), resource.id)
+ update_req = RequestFactory.Permission.add_req(permissions)
+ response = self.put_request(url, update_req)
+ permissions = PermissionsRule.from_response(response.content,
+ self.parent_srv.namespace)
+ logger.info('Updated permissions for resource {0}'.format(resource.id))
+
+ return permissions
+
+ def delete(self, resource, rules):
+ # Delete is the only endpoint that doesn't take a list of rules
+ # so let's fake it to keep it consistent
+ # TODO that means we need error handling around the call
+ if isinstance(rules, PermissionsRule):
+ rules = [rules]
+
+ for rule in rules:
+ for capability, mode in rule.capabilities.items():
+ " /permissions/groups/group-id/capability-name/capability-mode"
+ url = '{0}/{1}/permissions/{2}/{3}/{4}/{5}'.format(
+ self.owner_baseurl(),
+ resource.id,
+ rule.grantee.permissions_grantee_type + 's',
+ rule.grantee.id,
+ capability,
+ mode)
+
+ logger.debug('Removing {0} permission for capabilty {1}'.format(
+ mode, capability))
+
+ self.delete_request(url)
+
+ logger.info('Deleted permission for {0} {1} item {2}'.format(
+ rule.grantee.permissions_grantee_type,
+ rule.grantee.id,
+ resource.id))
+
+ def populate(self, item):
+ if not item.id:
+ error = "Server item is missing ID. Item must be retrieved from server first."
+ raise MissingRequiredFieldError(error)
+
+ def permission_fetcher():
+ return self._get_permissions(item)
+
+ item._set_permissions(permission_fetcher)
+ logger.info('Populated permissions for item (ID: {0})'.format(item.id))
+
+ def _get_permissions(self, item, req_options=None):
+ url = "{0}/{1}/permissions".format(self.owner_baseurl(), item.id)
+ server_response = self.get_request(url, req_options)
+ permissions = PermissionsRule.from_response(server_response.content,
+ self.parent_srv.namespace)
+
+ return permissions
diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py
index 8157e1f59..e4dafcbcc 100644
--- a/tableauserverclient/server/endpoint/projects_endpoint.py
+++ b/tableauserverclient/server/endpoint/projects_endpoint.py
@@ -1,12 +1,22 @@
-from .endpoint import Endpoint, api
+from .endpoint import api, Endpoint
from .exceptions import MissingRequiredFieldError
-from .. import RequestFactory, ProjectItem, PaginationItem
+from .permissions_endpoint import _PermissionsEndpoint
+from .default_permissions_endpoint import _DefaultPermissionsEndpoint
+
+from .. import RequestFactory, ProjectItem, PaginationItem, PermissionsRule, Permission
+
import logging
logger = logging.getLogger('tableau.endpoint.projects')
class Projects(Endpoint):
+ def __init__(self, parent_srv):
+ super(Projects, self).__init__(parent_srv)
+
+ self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
+ self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl)
+
@property
def baseurl(self):
return "{0}/sites/{1}/projects".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@@ -50,3 +60,51 @@ def create(self, project_item):
new_project = ProjectItem.from_response(server_response.content, self.parent_srv.namespace)[0]
logger.info('Created new project (ID: {0})'.format(new_project.id))
return new_project
+
+ @api(version='2.0')
+ def populate_permissions(self, item):
+ self._permissions.populate(item)
+
+ @api(version='2.0')
+ def update_permission(self, item, rules):
+ return self._permissions.update(item, rules)
+
+ @api(version='2.0')
+ def delete_permission(self, item, rules):
+ return self._permissions.delete(item, rules)
+
+ @api(version='2.1')
+ def populate_workbook_default_permissions(self, item):
+ self._default_permissions.populate_default_permissions(item, Permission.Resource.Workbook)
+
+ @api(version='2.1')
+ def populate_datasource_default_permissions(self, item):
+ self._default_permissions.populate_default_permissions(item, Permission.Resource.Datasource)
+
+ @api(version='3.4')
+ def populate_flow_default_permissions(self, item):
+ self._default_permissions.populate_default_permissions(item, Permission.Resource.Flow)
+
+ @api(version='2.1')
+ def update_workbook_default_permissions(self, item):
+ self._default_permissions.update_default_permissions(item, Permission.Resource.Workbook)
+
+ @api(version='2.1')
+ def update_datasource_default_permissions(self, item):
+ self._default_permissions.update_default_permissions(item, Permission.Resource.Datasource)
+
+ @api(version='3.4')
+ def update_flow_default_permissions(self, item):
+ self._default_permissions.update_default_permissions(item, Permission.Resource.Flow)
+
+ @api(version='2.1')
+ def delete_workbook_default_permissions(self, item):
+ self._default_permissions.delete_default_permissions(item, Permission.Resource.Workbook)
+
+ @api(version='2.1')
+ def delete_datasource_default_permissions(self, item):
+ self._default_permissions.delete_default_permissions(item, Permission.Resource.Datasource)
+
+ @api(version='3.4')
+ def delete_flow_default_permissions(self, item):
+ self._default_permissions.delete_default_permissions(item, Permission.Resource.Flow)
diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py
index 772ed79b9..445b0ccde 100644
--- a/tableauserverclient/server/endpoint/workbooks_endpoint.py
+++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py
@@ -1,5 +1,8 @@
from .endpoint import Endpoint, api, parameter_added_in
from .exceptions import InternalServerError, MissingRequiredFieldError
+from .endpoint import api, parameter_added_in, Endpoint
+from .permissions_endpoint import _PermissionsEndpoint
+from .exceptions import MissingRequiredFieldError
from .fileuploads_endpoint import Fileuploads
from .resource_tagger import _ResourceTagger
from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem
@@ -25,6 +28,7 @@ class Workbooks(Endpoint):
def __init__(self, parent_srv):
super(Workbooks, self).__init__(parent_srv)
self._resource_tagger = _ResourceTagger(parent_srv)
+ self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
@property
def baseurl(self):
@@ -216,6 +220,18 @@ def _get_wb_preview_image(self, workbook_item):
preview_image = server_response.content
return preview_image
+ @api(version='2.0')
+ def populate_permissions(self, item):
+ self._permissions.populate(item)
+
+ @api(version='2.0')
+ def update_permissions(self, resource, rules):
+ return self._permissions.update(resource, rules)
+
+ @api(version='2.0')
+ def delete_permission(self, item, capability_item):
+ return self._permissions.delete(item, capability_item)
+
# Publishes workbook. Chunking method if file over 64MB
@api(version="2.0")
@parameter_added_in(as_job='3.0')
diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py
index 6a3f811b6..b6739af6b 100644
--- a/tableauserverclient/server/request_factory.py
+++ b/tableauserverclient/server/request_factory.py
@@ -5,6 +5,8 @@
from requests.packages.urllib3.fields import RequestField
from requests.packages.urllib3.filepost import encode_multipart_formdata
+from ..models import UserItem, GroupItem, PermissionsRule
+
def _add_multipart(parts):
mime_multipart_parts = list()
@@ -145,32 +147,26 @@ def update_req(self, group_item, default_site_role):
class PermissionRequest(object):
- def _add_capability(self, parent_element, capability_set, mode):
- for capability_item in capability_set:
- capability_element = ET.SubElement(parent_element, 'capability')
- capability_element.attrib['name'] = capability_item
- capability_element.attrib['mode'] = mode
-
- def add_req(self, permission_item):
+ def add_req(self, rules):
xml_request = ET.Element('tsRequest')
permissions_element = ET.SubElement(xml_request, 'permissions')
- for user_capability in permission_item.user_capabilities:
- grantee_element = ET.SubElement(permissions_element, 'granteeCapabilities')
- grantee_capabilities_element = ET.SubElement(grantee_element, user_capability.User)
- grantee_capabilities_element.attrib['id'] = user_capability.grantee_id
- capabilities_element = ET.SubElement(grantee_element, 'capabilities')
- self._add_capability(capabilities_element, user_capability.allowed, user_capability.Allow)
- self._add_capability(capabilities_element, user_capability.denied, user_capability.Deny)
-
- for group_capability in permission_item.group_capabilities:
- grantee_element = ET.SubElement(permissions_element, 'granteeCapabilities')
- ET.SubElement(grantee_element, group_capability, id=group_capability.grantee_id)
- capabilities_element = ET.SubElement(grantee_element, 'capabilities')
- self._add_capability(capabilities_element, group_capability.allowed, group_capability.Allow)
- self._add_capability(capabilities_element, group_capability.denied, group_capability.Deny)
+ for rule in rules:
+ grantee_capabilities_element = ET.SubElement(permissions_element, 'granteeCapabilities')
+ grantee_element = ET.SubElement(grantee_capabilities_element, rule.grantee.tag_name)
+ grantee_element.attrib['id'] = rule.grantee.id
+
+ capabilities_element = ET.SubElement(grantee_capabilities_element, 'capabilities')
+ self._add_all_capabilities(capabilities_element, rule.capabilities)
+
return ET.tostring(xml_request)
+ def _add_all_capabilities(self, capabilities_element, capabilities_map):
+ for name, mode in capabilities_map.items():
+ capability_element = ET.SubElement(capabilities_element, 'capability')
+ capability_element.attrib['name'] = name
+ capability_element.attrib['mode'] = mode
+
class ProjectRequest(object):
def update_req(self, project_item):
diff --git a/test/assets/datasource_populate_permissions.xml b/test/assets/datasource_populate_permissions.xml
new file mode 100644
index 000000000..db967f4a9
--- /dev/null
+++ b/test/assets/datasource_populate_permissions.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/assets/project_populate_permissions.xml b/test/assets/project_populate_permissions.xml
new file mode 100644
index 000000000..7a49391af
--- /dev/null
+++ b/test/assets/project_populate_permissions.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/project_populate_workbook_default_permissions.xml b/test/assets/project_populate_workbook_default_permissions.xml
new file mode 100644
index 000000000..e6f3804be
--- /dev/null
+++ b/test/assets/project_populate_workbook_default_permissions.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/workbook_populate_permissions.xml b/test/assets/workbook_populate_permissions.xml
new file mode 100644
index 000000000..57517d719
--- /dev/null
+++ b/test/assets/workbook_populate_permissions.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/assets/workbook_update_permissions.xml b/test/assets/workbook_update_permissions.xml
new file mode 100644
index 000000000..fffd90491
--- /dev/null
+++ b/test/assets/workbook_update_permissions.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/test_datasource.py b/test/test_datasource.py
index 0563d2af7..fdf3c2e51 100644
--- a/test/test_datasource.py
+++ b/test/test_datasource.py
@@ -13,6 +13,7 @@
GET_EMPTY_XML = 'datasource_get_empty.xml'
GET_BY_ID_XML = 'datasource_get_by_id.xml'
POPULATE_CONNECTIONS_XML = 'datasource_populate_connections.xml'
+POPULATE_PERMISSIONS_XML = 'datasource_populate_permissions.xml'
PUBLISH_XML = 'datasource_publish.xml'
PUBLISH_XML_ASYNC = 'datasource_publish_async.xml'
UPDATE_XML = 'datasource_update.xml'
@@ -181,6 +182,32 @@ def test_update_connection(self):
self.assertEquals('9876', new_connection.server_port)
self.assertEqual('foo', new_connection.username)
+ def test_populate_permissions(self):
+ with open(asset(POPULATE_PERMISSIONS_XML), 'rb') as f:
+ response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.get(self.baseurl + '/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions', text=response_xml)
+ single_datasource = TSC.DatasourceItem('test')
+ single_datasource._id = '0448d2ed-590d-4fa0-b272-a2a8a24555b5'
+
+ self.server.datasources.populate_permissions(single_datasource)
+ permissions = single_datasource.permissions
+
+ self.assertEqual(permissions[0].grantee.tag_name, 'group')
+ self.assertEqual(permissions[0].grantee.id, '5e5e1978-71fa-11e4-87dd-7382f5c437af')
+ self.assertDictEqual(permissions[0].capabilities, {
+ TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny,
+ TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Deny,
+ TSC.Permission.Capability.Connect: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow,
+ })
+
+ self.assertEqual(permissions[1].grantee.tag_name, 'user')
+ self.assertEqual(permissions[1].grantee.id, '7c37ee24-c4b1-42b6-a154-eaeab7ee330a')
+ self.assertDictEqual(permissions[1].capabilities, {
+ TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow,
+ })
+
def test_publish(self):
response_xml = read_xml_asset(PUBLISH_XML)
with requests_mock.mock() as m:
diff --git a/test/test_project.py b/test/test_project.py
index c0958f761..6e055e50f 100644
--- a/test/test_project.py
+++ b/test/test_project.py
@@ -3,11 +3,15 @@
import requests_mock
import tableauserverclient as TSC
+from ._utils import read_xml_asset, read_xml_assets, asset
+
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
-GET_XML = os.path.join(TEST_ASSET_DIR, 'project_get.xml')
-UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'project_update.xml')
-CREATE_XML = os.path.join(TEST_ASSET_DIR, 'project_create.xml')
+GET_XML = asset('project_get.xml')
+UPDATE_XML = asset('project_update.xml')
+CREATE_XML = asset('project_create.xml')
+POPULATE_PERMISSIONS_XML = 'project_populate_permissions.xml'
+POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML = 'project_populate_workbook_default_permissions.xml'
class ProjectTests(unittest.TestCase):
@@ -97,3 +101,54 @@ def test_create(self):
def test_create_missing_name(self):
self.assertRaises(ValueError, TSC.ProjectItem, '')
+
+ def test_populate_permissions(self):
+ with open(asset(POPULATE_PERMISSIONS_XML), 'rb') as f:
+ response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.get(self.baseurl + '/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions', text=response_xml)
+ single_project = TSC.ProjectItem('Project3')
+ single_project._id = '0448d2ed-590d-4fa0-b272-a2a8a24555b5'
+
+ self.server.projects.populate_permissions(single_project)
+ permissions = single_project.permissions
+
+ self.assertEqual(permissions[0].grantee.tag_name, 'group')
+ self.assertEqual(permissions[0].grantee.id, 'c8f2773a-c83a-11e8-8c8f-33e6d787b506')
+ self.assertDictEqual(permissions[0].capabilities, {
+ TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow,
+ })
+
+ def test_populate_workbooks(self):
+ response_xml = read_xml_asset(POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML)
+ with requests_mock.mock() as m:
+ m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/workbooks',
+ text=response_xml)
+ single_project = TSC.ProjectItem('test', '1d0304cd-3796-429f-b815-7258370b9b74')
+ single_project.owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794'
+ single_project._id = '9dbd2263-16b5-46e1-9c43-a76bb8ab65fb'
+
+ self.server.projects.populate_workbook_default_permissions(single_project)
+ permissions = single_project.default_workbook_permissions
+
+ rule1 = permissions.pop()
+
+ self.assertEqual('c8f2773a-c83a-11e8-8c8f-33e6d787b506', rule1.grantee.id)
+ self.assertEqual('group', rule1.grantee.tag_name)
+ self.assertDictEqual(rule1.capabilities, {
+ TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny,
+ TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ViewUnderlyingData: TSC.Permission.Mode.Deny,
+ TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow,
+ })
diff --git a/test/test_sort.py b/test/test_sort.py
index 17a69e900..a6a57497d 100644
--- a/test/test_sort.py
+++ b/test/test_sort.py
@@ -57,8 +57,8 @@ def test_filter_in(self):
request_object=opts,
auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM',
content_type='text/xml')
-
- self.assertEqual(resp.request.query, 'pagenumber=13&pagesize=13&filter=tags:in:%5bstocks,market%5d')
+ self.assertDictEqual(resp.request.qs, {'pagenumber': ['13'], 'pagesize': [
+ '13'], 'filter': ['tags:in:[stocks,market]']})
def test_sort_asc(self):
with requests_mock.mock() as m:
diff --git a/test/test_workbook.py b/test/test_workbook.py
index ae814c0b2..0317ba115 100644
--- a/test/test_workbook.py
+++ b/test/test_workbook.py
@@ -7,6 +7,10 @@
from tableauserverclient.datetime_helpers import format_datetime
from tableauserverclient.server.endpoint.exceptions import InternalServerError
from tableauserverclient.server.request_factory import RequestFactory
+from tableauserverclient.models.permissions_item import PermissionsRule
+from tableauserverclient.models.user_item import UserItem
+from tableauserverclient.models.group_item import GroupItem
+
from ._utils import asset
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
@@ -17,12 +21,14 @@
GET_XML = os.path.join(TEST_ASSET_DIR, 'workbook_get.xml')
POPULATE_CONNECTIONS_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_connections.xml')
POPULATE_PDF = os.path.join(TEST_ASSET_DIR, 'populate_pdf.pdf')
+POPULATE_PERMISSIONS_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_permissions.xml')
POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, 'RESTAPISample Image.png')
POPULATE_VIEWS_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views.xml')
POPULATE_VIEWS_USAGE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views_usage.xml')
PUBLISH_XML = os.path.join(TEST_ASSET_DIR, 'workbook_publish.xml')
PUBLISH_ASYNC_XML = os.path.join(TEST_ASSET_DIR, 'workbook_publish_async.xml')
UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_update.xml')
+UPDATE_PERMISSIONS = os.path.join(TEST_ASSET_DIR, 'workbook_update_permissions.xml')
class WorkbookTests(unittest.TestCase):
@@ -270,6 +276,66 @@ def test_populate_connections(self):
self.assertEqual('4506225a-0d32-4ab1-82d3-c24e85f7afba', single_workbook.connections[0].datasource_id)
self.assertEqual('World Indicators', single_workbook.connections[0].datasource_name)
+ def test_populate_permissions(self):
+ with open(POPULATE_PERMISSIONS_XML, 'rb') as f:
+ response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.get(self.baseurl + '/21778de4-b7b9-44bc-a599-1506a2639ace/permissions', text=response_xml)
+ single_workbook = TSC.WorkbookItem('test')
+ single_workbook._id = '21778de4-b7b9-44bc-a599-1506a2639ace'
+
+ self.server.workbooks.populate_permissions(single_workbook)
+ permissions = single_workbook.permissions
+
+ self.assertEqual(permissions[0].grantee.tag_name, 'group')
+ self.assertEqual(permissions[0].grantee.id, '5e5e1978-71fa-11e4-87dd-7382f5c437af')
+ self.assertDictEqual(permissions[0].capabilities, {
+ TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow
+ })
+
+ self.assertEqual(permissions[1].grantee.tag_name, 'user')
+ self.assertEqual(permissions[1].grantee.id, '7c37ee24-c4b1-42b6-a154-eaeab7ee330a')
+ self.assertDictEqual(permissions[1].capabilities, {
+ TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow,
+ TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Deny,
+ TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Deny
+ })
+
+ def test_add_permissions(self):
+ with open(UPDATE_PERMISSIONS, 'rb') as f:
+ response_xml = f.read().decode('utf-8')
+
+ single_workbook = TSC.WorkbookItem('test')
+ single_workbook._id = '21778de4-b7b9-44bc-a599-1506a2639ace'
+
+ bob = UserItem.as_reference("7c37ee24-c4b1-42b6-a154-eaeab7ee330a")
+ group_of_people = GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af")
+
+ new_permissions = [
+ PermissionsRule(bob, {'Write': 'Allow'}),
+ PermissionsRule(group_of_people, {'Read': 'Deny'})
+ ]
+
+ with requests_mock.mock() as m:
+ m.put(self.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml)
+ permissions = self.server.workbooks.update_permissions(single_workbook, new_permissions)
+
+ self.assertEqual(permissions[0].grantee.tag_name, 'group')
+ self.assertEqual(permissions[0].grantee.id, '5e5e1978-71fa-11e4-87dd-7382f5c437af')
+ self.assertDictEqual(permissions[0].capabilities, {
+ TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny
+ })
+
+ self.assertEqual(permissions[1].grantee.tag_name, 'user')
+ self.assertEqual(permissions[1].grantee.id, '7c37ee24-c4b1-42b6-a154-eaeab7ee330a')
+ self.assertDictEqual(permissions[1].capabilities, {
+ TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow
+ })
+
def test_populate_connections_missing_id(self):
single_workbook = TSC.WorkbookItem('test')
self.assertRaises(TSC.MissingRequiredFieldError,