Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \
SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
SubscriptionItem, Target
SubscriptionItem, Target, PermissionsCollection, Permission, PermissionsRule, PermissionsGrantee

from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
from ._version import get_versions
Expand Down
1 change: 1 addition & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
from .view_item import ViewItem
from .workbook_item import WorkbookItem
from .subscription_item import SubscriptionItem
from .permissions_item import Permission, PermissionsCollection, PermissionsRule, PermissionsGrantee
4 changes: 4 additions & 0 deletions tableauserverclient/models/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
class UnpopulatedPropertyError(Exception):
pass


class UnknownGranteeTypeError(Exception):
pass
147 changes: 147 additions & 0 deletions tableauserverclient/models/permissions_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import xml.etree.ElementTree as ET
import logging

from .exceptions import UnknownGranteeTypeError


logger = logging.getLogger("tableau.models.permissions_item")


class Permission:
class GranteeType:
User = "user"
Group = "group"

class CapabilityMode:
Allow = "Allow"
Deny = "Deny"

class DatasourceCapabilityType:
ChangePermissions = "ChangePermissions"
Connect = "Connect"
Delete = "Delete"
ExportXml = "ExportXml"
Read = "Read"
Write = "Write"

class WorkbookCapabilityType:
AddComment = "AddComment"
ChangeHierarchy = "ChangeHierarchy"
ChangePermissions = "ChangePermissions"
Delete = "Delete"
ExportData = "ExportData"
ExportImage = "ExportImage"
ExportXml = "ExportXml"
Filter = "Filter"
Read = "Read"
ShareView = "ShareView"
ViewComments = "ViewComments"
ViewUnderlyingData = "ViewUnderlyingData"
WebAuthoring = "WebAuthoring"
Write = "Write"

class ProjectCapabilityType:
ProjectLeader = "ProjectLeader"
Read = "Read"
Write = "Write"


class PermissionsGrantee(object):
def __init__(self, grantee_type, grantee_id):

if grantee_type not in [
Permission.GranteeType.User,
Permission.GranteeType.Group,
]:
raise UnknownGranteeTypeError(grantee_type)

self._grantee_type = grantee_type
self._grantee_id = grantee_id

@classmethod
def from_xml_element(cls, xml_element):
tag_without_namespace = xml_element.tag.split("}")[-1]
return cls(tag_without_namespace, xml_element.get("id"))

def to_xml_element(self):
xml_element = ET.Element(self.grantee_type)
xml_element.set("id", self.grantee_id)
return xml_element

@property
def grantee_type(self):
return self._grantee_type

@property
def grantee_id(self):
return self._grantee_id


class PermissionsRule(object):
def __init__(self, grantee, permissions_map=None):
self._grantee = grantee
self.permissions_map = permissions_map or {}

@property
def grantee(self):
return self._grantee

def to_xml_element(self):
xml_element = ET.Element("granteeCapabilities")
xml_element.append(self.grantee.to_xml_element())
capabilities_element = ET.SubElement(xml_element, "capabilities")
for permission, mode in self.permissions_map.items():
ET.SubElement(
capabilities_element, "capability", {"name": permission, "mode": mode}
)
return xml_element


class PermissionsCollection(object):
def __init__(self, rules):
self._rules = rules

@property
def rules(self):
return self._rules

@classmethod
def from_response(cls, resp, ns=None):
parsed_response = ET.fromstring(resp)

capabilities = []
all_xml = parsed_response.findall(".//t:granteeCapabilities", namespaces=ns)

for grantee_capability_xml in all_xml:
user_grantee_element = grantee_capability_xml.find(
"./t:user", namespaces=ns
)
group_grantee_element = grantee_capability_xml.find(
"./t:group", namespaces=ns
)
grantee_element = (
user_grantee_element
if user_grantee_element is not None
else group_grantee_element
)

grantee = PermissionsGrantee.from_xml_element(grantee_element)

capability_elements = grantee_capability_xml.findall(
".//t:capabilities/t:capability", namespaces=ns
)
capability_map = {
el.get("name"): el.get("mode") for el in capability_elements
}

capability_item = PermissionsRule(grantee, capability_map)

capabilities.append(capability_item)

return cls(capabilities)

def to_xml_element(self):
xml_element = ET.Element("permissions")
for permission_rule in self.rules:
xml_element.append(permission_rule.to_xml_element())
return xml_element
1 change: 1 addition & 0 deletions tableauserverclient/models/project_item.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import xml.etree.ElementTree as ET
from .property_decorators import property_is_enum, property_not_empty
from .exceptions import UnpopulatedPropertyError


class ProjectItem(object):
Expand Down
11 changes: 11 additions & 0 deletions tableauserverclient/models/workbook_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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):
Expand All @@ -35,6 +36,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
Expand Down Expand Up @@ -120,6 +128,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

Expand Down
3 changes: 2 additions & 1 deletion tableauserverclient/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
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, PermissionsCollection, \
Permission, PermissionsRule, PermissionsGrantee
from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \
Sites, Users, Views, Workbooks, Subscriptions, ServerResponseError, \
MissingRequiredFieldError
Expand Down
76 changes: 76 additions & 0 deletions tableauserverclient/server/endpoint/permissions_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import logging

from .. import RequestFactory, PermissionsCollection

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 instantiated 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, item, permission_collection):
url = "{0}/{1}/permissions".format(self.owner_baseurl(), item.id)
update_req = RequestFactory.Permission.add_req(permission_collection)
response = self.put_request(url, update_req)
permissions = PermissionsCollection.from_response(
response.content, self.parent_srv.namespace
)

logger.info("Updated permissions for item {0}".format(item.id))

return permissions

def delete(self, item, permission_collection):
for rule in permission_collection.rules:
for capability_type, capability_mode in rule.permissions_map.items():
url = "{0}/{1}/permissions/{2}s/{3}/{4}/{5}".format(
self.owner_baseurl(),
item.id,
rule.grantee.grantee_type,
rule.grantee.grantee_id,
capability_type,
capability_mode,
)
self.delete_request(url)
logger.info(
"Deleted permission for {0} {1} item {2}".format(
rule.grantee.grantee_type, rule.grantee.grantee_id, item.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)

permissions = self._get_permissions(item)
item._set_permissions(permissions)
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 = PermissionsCollection.from_response(
server_response.content, self.parent_srv.namespace
)
return permissions
17 changes: 16 additions & 1 deletion tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .endpoint import Endpoint, api, parameter_added_in
from .endpoint import api, parameter_added_in, Endpoint
from .permissions_endpoint import _PermissionsEndpoint
from .exceptions import InternalServerError, MissingRequiredFieldError
from .fileuploads_endpoint import Fileuploads
from .resource_tagger import _ResourceTagger
Expand All @@ -25,6 +26,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):
Expand Down Expand Up @@ -216,6 +218,19 @@ 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_permission(self, item, permission_item):
permissions = self._permissions.update(item, permission_item)
item._set_permissions(permissions)

@api(version='2.0')
def delete_permission(self, item, capability_item):
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')
Expand Down
25 changes: 2 additions & 23 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,30 +142,9 @@ 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, permission_collection):
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)
xml_request.append(permission_collection.to_xml_element())
return ET.tostring(xml_request)


Expand Down
27 changes: 27 additions & 0 deletions test/assets/workbook_populate_permissions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
<permissions>
<workbook id="21778de4-b7b9-44bc-a599-1506a2639ace" name="ComplianceDashboard">
<owner id="91d5931a-2cd6-4dbc-8b7c-211e85cf5fa3" />
</workbook>
<granteeCapabilities>
<group id="5e5e1978-71fa-11e4-87dd-7382f5c437af" />
<capabilities>
<capability name="WebAuthoring" mode="Allow" />
<capability name="Read" mode="Allow" />
<capability name="Filter" mode="Allow" />
<capability name="AddComment" mode="Allow" />
</capabilities>
</granteeCapabilities>
<granteeCapabilities>
<user id="7c37ee24-c4b1-42b6-a154-eaeab7ee330a" />
<capabilities>
<capability name="ExportImage" mode="Allow" />
<capability name="ShareView" mode="Allow" />
<capability name="ExportData" mode="Deny" />
<capability name="ViewComments" mode="Deny" />
</capabilities>
</granteeCapabilities>
</permissions>
</tsResponse>

Loading