Setting, Auditing, and Modifying Permissions Programmatically Using tableau_tools

One of the biggest benefits of the Tableau Server REST API is that it allows you to set permissions programmatically. Tabcmd can’t do this, so it’s a feature unique to the REST API. In a multi-tenant deployment, it’s absolutely a necessity. But Tableau’s permissions are have a lot of options, and thus working with them in the REST API requires setting a lot of options. In the process of working through common customer use cases, I’ve ended up working through a lot of the complexities, with the aim to add useful shortcuts and examples so that others can more easily get started.

Section 4 – Permissions of the guide to the tableau_rest_api sub-package of the tableau_tools library covers the basics of the permissions functionality in the the tableau_rest_api package. In this post, I’ll give further examples of many common permissions requests.

Note: Please upgrade to the latest tableau_tools package (minimum 3.1.0) for the following to work.

Basics of Tableau Permissions

“Content Objects”: Projects, Workbooks, Data Sources

Tableau has three “Content Object” types: Projects, Workbooks, and Data Sources. Workbooks and Data Sources must belong to one and only one Project, but the Project they belong to can change.

In Tableau Server 9.2 and beyond, you can lock the permissions on a project, so that any Workbook or Data Source in the Project will always take the Default Permissions set on the Project. Additionally, the Default Permissions for a Workbook and a Data Source are stored as “sub-objects” of a Project:

  • Project
    • Default Workbook Permissions
    • Default Data Source Permissions

Best Practice: Lock the permissions on all Projects of “official” content. There are use cases, such as “sandbox” projects, where leaving the permissions up to the creator are acceptable, but anything that is intended to be “official” or “blessed” should always be in a Project where the permissions are locked.

Prior to Tableau Server 9.2, every content object had its own separate set of permissions. Workbooks and Data Sources would inherit a default set of permissions based on the settings for the Project they were published to, but the creator could assign any set of permissions to them. As you might imagine, this could provide an auditing nightmare. In particular, a workbook created in a project with one set of permissions could be moved to a different project with different permissions, but the workbook would keep its original permission. If you are still in this world, the REST API can be used to audit and ensure that content within the Project matches to the set project permissions. In 9.1 and before, a Project has ALL of the possible permissions, without the concept of the default permissions objects.

“Security Grantees”: Users and Groups

The second aspect to remember about Tableau Server permissions is that the permissions for each Content Object can be set individually for Users or for Groups, referred to generically as “Grantees”.

Best Practice: Always set security on Projects using Groups, with no individual users (except for the Guest user, if you have it enabled, you may need to block access on some Projects). Manage access by adding or removing people from Groups. Adding and removing from Groups is VASTLY easier than setting Permissions, making everything easier to do and audit.

There’s nothing fundamentally different between setting permissions for Users or Groups.

Capabilities: The actual Permissions

The Tableau REST API refers to each individual “permission” as a “Capability”, and these can have one of three states: Allow, Deny, or Unspecified. If a capability is unspecified, it won’t actually return back in the REST API. This means a request for a set of ‘granteeCapabilities’ won’t return the full set of possible capabilities unless they are all set to one of Allow or Deny.

One other quirk is that once a Capability is set to Allow or Deny, there is no Update action. Instead, you must delete the existing Capability and then add the new Capability. Lastly, the names of the Capabilities in the REST API vary from what you see in the Tableau Server UI; the list of translations is that bottom of this page.

Adding Permissions to a New Project

When you add permissions for the first time, you don’t have to worry much about the quirks that affect changing existing permissions. This example is for deploying a set of new sites (the example starts with deployment):

logger = Logger(u'scratch.log')

server = ''
username = ''
password = ''
d = TableauRestApiConnection(server, username, password)

sites_to_create = {'site_1': 'Site 1', 'site_2': 'Site 2', 'site_3': 'Site 3'}
# Create each of the sites
for site in sites_to_create:
d.create_site(sites_to_create[site], site)
t = TableauRestApiConnection(server, username, password, site)
groups_to_create = ['Group 1', 'Group 2', 'Group 3']
projects_to_create = ['Project 1', 'Project 2', 'Project 3']
groups_dict = {}
for group in groups_to_create:
# When you create, a LUID is returned. We store them here to use for further commands
groups_dict[group] = t.create_group(group)
projects_dict = {}

# When a project is created, it takes the settings of the Default project
# Set All Users to Undefined
all_users_luid = t.query_group_luid_by_name(u'All Users')
default_project_luid = t.query_project_luid_by_name(u'Default')
default_proj_obj = t.get_project_object_by_luid(default_project_luid)

gcap_obj_p = t.get_grantee_capabilities_object(u'group', all_users_luid, u'project')

gcap_obj_ds = t.get_grantee_capabilities_object(u'group', all_users_luid, u'workbook')

gcap_obj_wb = t.get_grantee_capabilities_object(u'group', all_users_luid, u'workbook')

for project in projects_to_create:
# Deploy them locked
# When you create, a LUID is returned. We store them here to use for further commands
projects_dict[project] = t.create_project(project, locked_permissions=True)

# Project Object represents the settings of the publish project
proj_obj = t.get_project_object_by_luid(projects_dict[project])

# Each set of permissions is represented by one GranteeCapabilities object
# Project class can do testing and comparison if send them one at a time
g1_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 1'], u'project')

g2_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 2'], u'project')

g3_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 3'], u'project')

# Also need to set the Default Workbook permissions
g1_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 1'], u'workbook')

g2_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 2'], u'workbook')

g3_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 3'], u'workbook')

# Also need to set the Default Data Source permissions
g1_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 1'], u'datasource')

g2_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 2'], u'datasource')

g3_gcap_obj = t.get_grantee_capabilities_object(u'group', groups_dict['Group 3'], u'datasource')

You’ll notice a few things:

  • There are quite a few time.sleep() calls: When you create things using the REST API, it takes the Tableau Server a few seconds to update its internal indexes, so if you send a command to a newly created Project immediately, you might get back an error.
  • The first set of commands actually takes away all of the default permissions from the Default Project. If you don’t do this, each subsequently created Project will start life with the settings from the Default Project.
  • You must set the Project permissions, the Default Workbook Permissions, and the Default Datasource Permissions, each with their own commands

Part of the magic in the GranteeCapabilities class is that the Roles from Tableau Server are built in, so you can use the set_capabilities_to_match_role() method. You can add or delete from the role after you have used this method, using the set_capability(capability_name, mode) or set_capability_to_unspecified(capability_name) methods. The mode is either ‘Allow’ or ‘Deny’.

You can then use the set_permissions_by_gcap_obj(GranteeCapabilities) method to assign that set of capabilities to the Project (or the workbook_default or datasource_default objects that belong to the Project).

Changing Existing Permissions

As may have been belabored in other posts, there is no real update method for permissions. You’ve got to delete out anything that exists and then reset with the permissions you really want. Luckily, the set_permissions_by_gcap_obj() method handles determining any differences and doing all the necessary work.  Simply put, it tests for existing permissions for that Grantee, sees if they are different from the new ones, and if so, clears all the old permissions so that the new ones can be set. This also works to clear existing permissions — if you set all of the capabilities to Unspecified, the old ones will all be deleted, with nothing new set (as seen in the example above).

Clearing out Unknown Permissions (Setting a “Blank Slate”)

The PublishedContent class, which Project, Workbook and Datasource all inherit from, implements a method called clear_all_permissions(), which can be used prior to setting new permissions. Using this, you don’t have to worry about anything that was previously set. This allows for a full reset of your permissions on existing sites. For a Project, you can also use clear_all_permissions_including_defaults() to clear everything related to that Project.

Auditing Permissions

So you’ve got a great script that goes through and makes all of the changes to permissions that you want, but you might want to audit across the whole of a site (or even all of your sites) to make sure you have the similar structure for Groups and Projects in place across all of them. The Project class has a method specifically for this called query_all_permissions() which returns a dict in the format

{ luid : { permissions, workbook_default_permission, datsource_default_permissions} , … }

There is another method that can work with this object to present all of the permissions and default permissions as one array in the same order that permissions appear in Tableau Server. The following example script (also available in the tableau_tools/examples directory as “” will output a CSV for all of the Projects on the all the Sites on the Tableau Server.

# -*- coding: utf-8 -*-
from tableau_tools.tableau_rest_api import *
from tableau_tools.tableau_rest_api.published_content import Project, Workbook, Datasource
from tableau_tools import *

username = ''
password = ''
server = 'http://localhost'

logger = Logger('permissions.log')
default = TableauRestApiConnection(server, username, password)

output_file = open('permissions_audit.txt', 'wb')
# Get all sites content urls for logging in
site_content_urls = default.query_all_site_content_urls()

# Headers
output_file.write(u'Site Content URL,Project Name,Project LUID,Principal Type,Principal Name,Principal LUID')
project_caps = default.available_capabilities[default.api_version]['project']
for cap in project_caps:
workbook_caps = default.available_capabilities[default.api_version]['workbook']
for cap in workbook_caps:
datasource_caps = default.available_capabilities[default.api_version]['datasource']
for cap in datasource_caps:

for site_content_url in site_content_urls:
t = TableauRestApiConnection(server, username, password, site_content_url)
projects = t.query_projects()
projects_dict = t.convert_xml_list_to_name_id_dict(projects)
for project in projects_dict:
# combined_permissions = luid : {type, name, proj, def_wb, def_ds}
gcap_combined_permissions = {}
proj_obj = t.get_project_object_by_luid(projects_dict[project])
# is_locked = proj_obj.
all_perms = proj_obj.query_all_permissions()

for luid in all_perms:
if site_content_url is None:
site_content_url = ''
output_file.write(u",{},{}".format(project, projects_dict[project]).encode('utf-8'))
output_file.write(u",{},{},{}".format(all_perms[luid]["type"], all_perms[luid]["name"], luid).encode('utf-8'))
all_perms_list = proj_obj.convert_all_permissions_to_list(all_perms[luid])
for perm in all_perms_list:


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s