Skip to content

Commit a412777

Browse files
DepthDeluxeColin Heinzmann
authored andcommitted
adding permissions support for workbook, datasource, project
* _PermissionsEndpoint to be used inside Endpoint that has the calls * Added a couple of classes related to permissions, followed naming convention of API
1 parent 48df0ef commit a412777

16 files changed

+442
-23
lines changed

tableauserverclient/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
GroupItem, JobItem, PaginationItem, ProjectItem, ScheduleItem, \
44
SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
55
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
6-
SubscriptionItem
6+
SubscriptionItem, PermissionsItem, Permission, GranteeCapabilityItem
77
from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
88
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
99
from ._version import get_versions

tableauserverclient/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616
from .view_item import ViewItem
1717
from .workbook_item import WorkbookItem
1818
from .subscription_item import SubscriptionItem
19+
from .permissions_item import Permission, PermissionsItem, GranteeCapabilityItem

tableauserverclient/models/datasource_item.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ def __init__(self, project_id, name=None):
2323
self.project_id = project_id
2424
self.tags = set()
2525

26+
self._permissions = None
27+
2628
@property
2729
def connections(self):
2830
if self._connections is None:
2931
error = 'Datasource item must be populated with connections first.'
3032
raise UnpopulatedPropertyError(error)
3133
return self._connections()
3234

35+
@property
36+
def permissions(self):
37+
if self._permissions is None:
38+
error = "Project item must be populated with permissions first."
39+
raise UnpopulatedPropertyError(error)
40+
return self._permissions()
41+
3342
@property
3443
def content_url(self):
3544
return self._content_url
@@ -84,6 +93,9 @@ def updated_at(self):
8493
def _set_connections(self, connections):
8594
self._connections = connections
8695

96+
def _set_permissions(self, permissions):
97+
self._permissions = permissions
98+
8799
def _parse_common_elements(self, datasource_xml, ns):
88100
if not isinstance(datasource_xml, ET.Element):
89101
datasource_xml = ET.fromstring(datasource_xml).find('.//t:datasource', namespaces=ns)
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import xml.etree.ElementTree as ET
2+
3+
4+
class ItemWithPermissions(object):
5+
pass
6+
7+
8+
class Permission:
9+
class Type:
10+
Datasource = 'datasource'
11+
Workbook = 'workbook'
12+
Project = 'project'
13+
14+
class GranteeType:
15+
User = 'user'
16+
Group = 'group'
17+
18+
class CapabilityMode:
19+
Allow = 'Allow'
20+
Deny = 'Deny'
21+
22+
class DatasourceCapabilityType:
23+
ChangePermissions = 'ChangePermissions'
24+
Connect = 'Connect'
25+
Delete = 'Delete'
26+
ExportXml = 'ExportXml'
27+
Read = 'Read'
28+
Write = 'Write'
29+
30+
class WorkbookCapabilityType:
31+
AddComment = 'AddComment'
32+
ChangeHierarchy = 'ChangeHierarchy'
33+
ChangePermissions = 'ChangePermissions'
34+
Delete = 'Delete'
35+
ExportData = 'ExportData'
36+
ExportImage = 'ExportImage'
37+
ExportXml = 'ExportXml'
38+
Filter = 'Filter'
39+
Read = 'Read'
40+
ShareView = 'ShareView'
41+
ViewComments = 'ViewComments'
42+
ViewUnderlyingData = 'ViewUnderlyingData'
43+
WebAuthoring = 'WebAuthoring'
44+
Write = 'Write'
45+
46+
class ProjectCapabilityType:
47+
ProjectLeader = 'ProjectLeader'
48+
Read = 'Read'
49+
Write = 'Write'
50+
51+
52+
class GranteeCapabilityItem(object):
53+
def __init__(self, grantee_type=None, grantee_id=None, capabilities=None):
54+
self._grantee_type = grantee_type
55+
self._grantee_id = grantee_id
56+
self._capabilities = capabilities
57+
58+
def _set_values(self, grantee_type, grantee_id, capabilities):
59+
self._grantee_type = grantee_type
60+
self._grantee_id = grantee_id
61+
self._capabilities = capabilities
62+
63+
@property
64+
def grantee_type(self):
65+
return self._grantee_type
66+
67+
@property
68+
def grantee_id(self):
69+
return self._grantee_id
70+
71+
@property
72+
def capabilities(self):
73+
return self._capabilities
74+
75+
76+
class PermissionsItem(object):
77+
def __init__(self):
78+
self._type = None
79+
self._item_id = None
80+
self._grantee_capabilities = None
81+
82+
def _set_values(self, type, item_id, grantee_capabilities):
83+
self._type = type
84+
self._item_id = item_id
85+
self._grantee_capabilities = grantee_capabilities
86+
87+
@property
88+
def type(self):
89+
return self._type
90+
91+
@property
92+
def item_id(self):
93+
return self._item_id
94+
95+
@property
96+
def grantee_capabilities(self):
97+
return self._grantee_capabilities
98+
99+
@property
100+
def is_user_permission(self):
101+
return self._user_id is not None
102+
103+
@property
104+
def is_group_permission(self):
105+
return self._group_id is not None
106+
107+
@classmethod
108+
def from_response(cls, resp, ns=None):
109+
permissions = PermissionsItem()
110+
parsed_response = ET.fromstring(resp)
111+
112+
for option in ('workbook', 'datasource', 'project'):
113+
try:
114+
item_id = parsed_response.find('.//t:{0}'.format(option), namespaces=ns).get('id')
115+
permission_type = option
116+
break
117+
except AttributeError:
118+
pass
119+
120+
all_xml = parsed_response.findall('.//t:granteeCapabilities', namespaces=ns)
121+
122+
grantee_capabilities = []
123+
for grantee_capability_xml in all_xml:
124+
grantee_capability = GranteeCapabilityItem()
125+
126+
try:
127+
grantee_id = grantee_capability_xml.find('.//t:group', namespaces=ns).get('id')
128+
grantee_type = Permission.GranteeType.Group
129+
except AttributeError:
130+
pass
131+
132+
try:
133+
grantee_id = grantee_capability_xml.find('.//t:user', namespaces=ns).get('id')
134+
grantee_type = Permission.GranteeType.User
135+
except AttributeError:
136+
pass
137+
138+
assert grantee_id is not None
139+
140+
capabilities = {}
141+
for capability_xml in grantee_capability_xml.findall('.//t:capabilities/t:capability', namespaces=ns):
142+
name = capability_xml.get('name')
143+
mode = capability_xml.get('mode')
144+
145+
capabilities[name] = mode
146+
147+
grantee_capability._set_values(grantee_type, grantee_id, capabilities)
148+
grantee_capabilities.append(grantee_capability)
149+
150+
permissions._set_values(permission_type, item_id, grantee_capabilities)
151+
return permissions

tableauserverclient/models/project_item.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import xml.etree.ElementTree as ET
22
from .property_decorators import property_is_enum, property_not_empty
3+
from .exceptions import UnpopulatedPropertyError
34

45

56
class ProjectItem(object):
@@ -15,10 +16,19 @@ def __init__(self, name, description=None, content_permissions=None, parent_id=N
1516
self.content_permissions = content_permissions
1617
self.parent_id = parent_id
1718

19+
self._permissions = None
20+
1821
@property
1922
def content_permissions(self):
2023
return self._content_permissions
2124

25+
@property
26+
def permissions(self):
27+
if self._permissions is None:
28+
error = "Project item must be populated with permissions first."
29+
raise UnpopulatedPropertyError(error)
30+
return self._permissions()
31+
2232
@content_permissions.setter
2333
@property_is_enum(ContentPermissions)
2434
def content_permissions(self, value):
@@ -61,6 +71,9 @@ def _set_values(self, project_id, name, description, content_permissions, parent
6171
if parent_id:
6272
self.parent_id = parent_id
6373

74+
def _set_permissions(self, permissions):
75+
self._permissions = permissions
76+
6477
@classmethod
6578
def from_response(cls, resp, ns):
6679
all_project_items = list()

tableauserverclient/models/workbook_item.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .property_decorators import property_not_nullable, property_is_boolean
44
from .tag_item import TagItem
55
from .view_item import ViewItem
6+
from .permissions_item import PermissionsItem
67
from ..datetime_helpers import parse_datetime
78
import copy
89

@@ -24,6 +25,7 @@ def __init__(self, project_id, name=None, show_tabs=False):
2425
self.project_id = project_id
2526
self.show_tabs = show_tabs
2627
self.tags = set()
28+
self._permissions = None
2729

2830
@property
2931
def connections(self):
@@ -32,6 +34,13 @@ def connections(self):
3234
raise UnpopulatedPropertyError(error)
3335
return self._connections()
3436

37+
@property
38+
def permissions(self):
39+
if self._permissions is None:
40+
error = "Workbook item must be populated with permissions first."
41+
raise UnpopulatedPropertyError(error)
42+
return self._permissions()
43+
3544
@property
3645
def content_url(self):
3746
return self._content_url
@@ -101,6 +110,9 @@ def views(self):
101110
def _set_connections(self, connections):
102111
self._connections = connections
103112

113+
def _set_permissions(self, permissions):
114+
self._permissions = permissions
115+
104116
def _set_views(self, views):
105117
self._views = views
106118

tableauserverclient/server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .sort import Sort
55
from .. import ConnectionItem, DatasourceItem, JobItem, \
66
GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\
7-
UserItem, ViewItem, WorkbookItem, TaskItem, SubscriptionItem
7+
UserItem, ViewItem, WorkbookItem, TaskItem, SubscriptionItem, PermissionsItem
88
from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \
99
Sites, Users, Views, Workbooks, Subscriptions, ServerResponseError, \
1010
MissingRequiredFieldError

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from .endpoint import Endpoint, api, parameter_added_in
1+
from .endpoint import api, parameter_added_in, Endpoint
2+
from .permissions_endpoint import _PermissionsEndpoint
23
from .exceptions import MissingRequiredFieldError
34
from .fileuploads_endpoint import Fileuploads
45
from .resource_tagger import _ResourceTagger
@@ -24,6 +25,7 @@ class Datasources(Endpoint):
2425
def __init__(self, parent_srv):
2526
super(Datasources, self).__init__(parent_srv)
2627
self._resource_tagger = _ResourceTagger(parent_srv)
28+
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
2729

2830
@property
2931
def baseurl(self):
@@ -196,3 +198,15 @@ def publish(self, datasource_item, file_path, mode, connection_credentials=None,
196198
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
197199
logger.info('Published {0} (ID: {1})'.format(filename, new_datasource.id))
198200
return new_datasource
201+
202+
@api(version='2.0')
203+
def populate_permissions(self, item):
204+
self._permissions.populate(item)
205+
206+
@api(version='2.0')
207+
def update_permission(self, item, permission_item):
208+
self._permissions.update(item, permission_item)
209+
210+
@api(version='2.0')
211+
def delete_permission(self, item, grantee_capability):
212+
self._permissions.delete(item, grantee_capability)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import logging
2+
3+
from .. import RequestFactory, PermissionsItem
4+
5+
from .endpoint import Endpoint, api
6+
from .exceptions import MissingRequiredFieldError
7+
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class _PermissionsEndpoint(Endpoint):
13+
''' Adds permission model to another endpoint
14+
15+
Tableau permissions model is identical between objects but they are nested under
16+
the parent object endpoint (i.e. permissions for workbooks are under
17+
/workbooks/:id/permission). This class is meant to be instantated inside a
18+
parent endpoint which has these supported endpoints
19+
'''
20+
def __init__(self, parent_srv, owner_baseurl):
21+
super(_PermissionsEndpoint, self).__init__(parent_srv)
22+
23+
# owner_baseurl is the baseurl of the parent. The MUST be a lambda
24+
# since we don't know the full site URL until we sign in. If
25+
# populated without, we will get a sign-in error
26+
self.owner_baseurl = owner_baseurl
27+
28+
def update(self, item, permission_item):
29+
url = '{0}/{1}/permissions'.format(self.owner_baseurl(), item.id)
30+
update_req = RequestFactory.Permission.add_req(permission_item)
31+
response = self.put_request(url, update_req)
32+
permissions = PermissionsItem.from_response(response.content,
33+
self.parent_srv.namespace)
34+
35+
logger.info('Updated permissions for item {0}'.format(item.id))
36+
37+
return permissions
38+
39+
def delete(self, item, grantee_capability):
40+
for capability_type in grantee_capability.capabilities:
41+
capability_mode = grantee_capability.capabilities[capability_type]
42+
43+
url = '{0}/{1}/permissions/{2}/{3}/{4}/{5}'.format(
44+
self.owner_baseurl(), item.id,
45+
grantee_capability.grantee_type + 's',
46+
grantee_capability.grantee_id, capability_type,
47+
capability_mode)
48+
49+
logger.debug('Removing {0} permission for capabilty {1}'.format(
50+
capability_mode, capability_type))
51+
52+
self.delete_request(url)
53+
54+
logger.info('Deleted permission for {0} {1} item {2}'.format(
55+
grantee_capability.type,
56+
grantee_capability.grantee_id,
57+
item.id))
58+
59+
def populate(self, item):
60+
if not item.id:
61+
error = "Server item is missing ID. Item must be retrieved from server first."
62+
raise MissingRequiredFieldError(error)
63+
64+
def permission_fetcher():
65+
return self._get_permissions(item)
66+
67+
item._set_permissions(permission_fetcher)
68+
logger.info('Populated permissions for item (ID: {0})'.format(item.id))
69+
70+
def _get_permissions(self, item, req_options=None):
71+
url = "{0}/{1}/permissions".format(self.owner_baseurl(), item.id)
72+
server_response = self.get_request(url, req_options)
73+
permissions = PermissionsItem.from_response(server_response.content, self.parent_srv.namespace)
74+
return permissions

0 commit comments

Comments
 (0)