Skip to content

Commit f2e852c

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 f2e852c

17 files changed

+423
-26
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, CapabilityItem
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, CapabilityItem

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
class UnpopulatedPropertyError(Exception):
22
pass
3+
4+
5+
class UnknownGranteeTypeError(Exception):
6+
pass
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import xml.etree.ElementTree as ET
2+
import logging
3+
4+
from .exceptions import UnknownGranteeTypeError
5+
6+
7+
logger = logging.getLogger('tableau.models.permissions_item')
8+
9+
10+
class Permission:
11+
class GranteeType:
12+
User = 'user'
13+
Group = 'group'
14+
15+
class CapabilityMode:
16+
Allow = 'Allow'
17+
Deny = 'Deny'
18+
19+
class DatasourceCapabilityType:
20+
ChangePermissions = 'ChangePermissions'
21+
Connect = 'Connect'
22+
Delete = 'Delete'
23+
ExportXml = 'ExportXml'
24+
Read = 'Read'
25+
Write = 'Write'
26+
27+
class WorkbookCapabilityType:
28+
AddComment = 'AddComment'
29+
ChangeHierarchy = 'ChangeHierarchy'
30+
ChangePermissions = 'ChangePermissions'
31+
Delete = 'Delete'
32+
ExportData = 'ExportData'
33+
ExportImage = 'ExportImage'
34+
ExportXml = 'ExportXml'
35+
Filter = 'Filter'
36+
Read = 'Read'
37+
ShareView = 'ShareView'
38+
ViewComments = 'ViewComments'
39+
ViewUnderlyingData = 'ViewUnderlyingData'
40+
WebAuthoring = 'WebAuthoring'
41+
Write = 'Write'
42+
43+
class ProjectCapabilityType:
44+
ProjectLeader = 'ProjectLeader'
45+
Read = 'Read'
46+
Write = 'Write'
47+
48+
49+
class CapabilityItem(object):
50+
def __init__(self, type=None, object_id=None, map={}):
51+
self._type = type
52+
self._object_id = object_id
53+
self.map = map
54+
55+
@property
56+
def type(self):
57+
return self._type
58+
59+
@property
60+
def object_id(self):
61+
return self._object_id
62+
63+
64+
class PermissionsItem(object):
65+
def __init__(self):
66+
self._capabilities = None
67+
68+
def _set_values(self, capabilities):
69+
self._capabilities = capabilities
70+
71+
@property
72+
def capabilities(self):
73+
return self._capabilities
74+
75+
@classmethod
76+
def from_response(cls, resp, ns=None):
77+
permissions = PermissionsItem()
78+
parsed_response = ET.fromstring(resp)
79+
80+
capabilities = {}
81+
all_xml = parsed_response.findall('.//t:granteeCapabilities',
82+
namespaces=ns)
83+
84+
for grantee_capability_xml in all_xml:
85+
grantee_id = None
86+
grantee_type = None
87+
capability_map = {}
88+
89+
try:
90+
grantee_id = grantee_capability_xml.find('.//t:group',
91+
namespaces=ns)\
92+
.get('id')
93+
grantee_type = Permission.GranteeType.Group
94+
except AttributeError:
95+
pass
96+
try:
97+
grantee_id = grantee_capability_xml.find('.//t:user',
98+
namespaces=ns)\
99+
.get('id')
100+
grantee_type = Permission.GranteeType.User
101+
except AttributeError:
102+
pass
103+
104+
if grantee_id is None:
105+
logger.error('Cannot find grantee type in response')
106+
raise UnknownGranteeTypeError()
107+
108+
for capability_xml in grantee_capability_xml.findall(
109+
'.//t:capabilities/t:capability', namespaces=ns):
110+
name = capability_xml.get('name')
111+
mode = capability_xml.get('mode')
112+
113+
capability_map[name] = mode
114+
115+
capability_item = CapabilityItem(grantee_type, grantee_id,
116+
capability_map)
117+
capabilities[(grantee_type, grantee_id)] = capability_item
118+
119+
permissions._set_values(capabilities)
120+
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, capability_item):
212+
self._permissions.delete(item, capability_item)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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(item, 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, capability_item):
40+
for capability_type, capability_mode in capability_item.map.items():
41+
url = '{0}/{1}/permissions/{2}/{3}/{4}/{5}'.format(
42+
self.owner_baseurl(), item.id,
43+
capability_item.type + 's',
44+
capability_item.object_id, capability_type,
45+
capability_mode)
46+
47+
logger.debug('Removing {0} permission for capabilty {1}'.format(
48+
capability_mode, capability_type))
49+
50+
self.delete_request(url)
51+
52+
logger.info('Deleted permission for {0} {1} item {2}'.format(
53+
capability_item.type,
54+
capability_item.object_id,
55+
item.id))
56+
57+
def populate(self, item):
58+
if not item.id:
59+
error = "Server item is missing ID. Item must be retrieved from server first."
60+
raise MissingRequiredFieldError(error)
61+
62+
def permission_fetcher():
63+
return self._get_permissions(item)
64+
65+
item._set_permissions(permission_fetcher)
66+
logger.info('Populated permissions for item (ID: {0})'.format(item.id))
67+
68+
def _get_permissions(self, item, req_options=None):
69+
url = "{0}/{1}/permissions".format(self.owner_baseurl(), item.id)
70+
server_response = self.get_request(url, req_options)
71+
permissions = PermissionsItem.from_response(server_response.content,
72+
self.parent_srv.namespace)
73+
return permissions

0 commit comments

Comments
 (0)