Using tableau_tools to change Permissions

In tableau_tools 4.2.3, there is a new example called permissions_changing.py . The examples create_site_sample.py and permissions_auditing.py show how to set permissions when you are working with a new site or to look at the permissions that exist on an existing site, but there was not previously an example of updating an existing site, where you might have existing permissions of any sort. The script itself is decently commented, but here I want to explore some of the things I had to think about when putting the script together, to help people doing other variations of this task. Read through the script, which is reasonably well commented, then come back and read more to gain a more full understanding.

Workbook Defaults vs. Workbook-level Permissions

Tableau introduced the concept of “Locked Permissions” in version 9.2 of Tableau Server, where any content within a Project will automatically take the permissions for workbooks or datasources specified on the Project’s Permissions page. This is tremendously useful from an administrative standpoint, and is a recommend best practice, however it is still not the default for sites and projects on Tableau Server. This means a lot of current Tableau deployments allow the content Publisher to set their own permissions at publish time.

If you are going to change to Locked Permissions on an existing project, take the following into consideration:

  • If you Lock Permissions on a Project with existing content, ALL workbooks and datasources will take the Permissions from the Project-level defaults.
    • You can accomplish this with the Project.lock_permissions() method
  • You MUST make sure that whatever your new permissions you’ve set on the project don’t give unexpected access to someone who was previously denied access at the workbook level
  • I suggest creating a new project and moving the workbooks over one by one.

Even if you don’t lock the permissions on a project, you most likely still want to update the defaults so that your content publishers are guided in the right direction. The basics of the script look like the following:

try:
    project_object = t.query_project(project)
except NoMatchFoundException:
    print(u"No project found with the given name, check the log")
    exit()

workbook_defaults_obj = project_object.workbook_defaults
print(u"Updating the Project's Workbook Defaults")
update_workbook_permissions(project_object, workbook_defaults_obj, all_users_group_luid, capabilities_to_set)

# Update the workbooks themselves (if the permissions aren't locked, because this would be a waste of time)
if project_object.are_permissions_locked() is False:
    wbs_in_project = t.query_workbooks_in_project(project)
    wbs_dict = t.convert_xml_list_to_name_id_dict(wbs_in_project)
    for wb in wbs_dict:
        # Second parameter project_name is unecessary when passing a LUID
        # That is why you reference wbs_dict[wb], rather than wb directly, which is just the name
        wb_obj = t.get_published_workbook_object(wbs_dict[wb], u"")
        print(u'Updating workbook with LUID {}'.format(wbs_dict[wb]))
        update_workbook_permissions(project_object, wb_obj, all_users_group_luid, capabilities_to_set)<span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

You’ll note that it updates the workbook defaults, then checks to see if the permissions are locked, and if not, goes through all of the workbooks in that project to make the change.

In tableau_tools, the Project.workbook_defaults property is actually a Workbook object from the published_content portion of the library. Similarly, Project.datasource_defaults is a Datasource object. That lets us construct a function that updates any given workbook (or datasource) or the defaults using the exact same code

def update_workbook_permissions(project_obj, published_workbook_object, group_luid, capabilities_dict):
    """
    :type project_obj: Project
    :type published_workbook_object: Workbook
    :type group_luid: unicode
    :type capabilities_dict: dict
    :return:
    """
    # Query the permissions objects (comes as a list)
    permissions = published_workbook_object.get_permissions_obj_list()
    print(u"Retrieved Permissions")
    # Get the permissions object for "All Users"
    # Have to check for "All Users" not being set at all
    does_group_have_any_permissions = False
    for perm_obj in permissions:

        if perm_obj.luid == group_luid:
            does_group_have_any_permissions = True
            for cap in capabilities_dict:
                perm_obj.set_capability(cap, capabilities_dict[cap])

    # Send back the whole of the original list of permissions, with the one modified.
    if does_group_have_any_permissions is True:
        print(u'Updating Existing Permissions for Group')
        published_workbook_object.set_permissions_by_permissions_obj_list(permissions)

    # If there are no permissions at all, create Permissions object for it
    elif does_group_have_any_permissions is False:
        new_perm_obj = project_obj.create_workbook_permissions_object_for_group(all_users_group_luid)
        for cap in capabilities_dict:
            new_perm_obj.set_capability(cap, capabilities_dict[cap])
        print(u'No permissions found for group, adding new permissions')
        published_workbook_object.set_permissions_by_permissions_obj_list([new_perm_obj, ])<span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

Permissions are Complex (in several ways)

You can control Permissions completely using the REST API, but Permissions themselves are relatively complex. Consider the following:

  • In Tableau Server 9.2 and later, a Project has its own project-level permissions, but also the Default Permissions for Workbooks and Data Sources
    • Although these are all displayed in a line in the Tableau Server UI, they are separate objects in the REST API and must be read and set separately from the Project’s own permissions
  • Permissions can be set at the Group or User level
  • If a Group or User does not have any specifically set permissions for a given piece of content, a query for the Permissions will not return that user or group at all
    • This includes the ‘All Users’ Group, which is always visible in the Tableau Server UI, even when there are no permissions set
  • When you query Permissions for an Project, Workbook , you receive a set of all of the Permission that are set for any Group or User
    • However, the users and groups are only referred to by their locally-unique identifiers (LUIDs)
  • There are a lot of Permissions for each content type, but within the REST API they are called Capabilities, and they have different names than what appears in the Tableau Server UI
  • Permissions which are set cannot be updated / overwritten without first being deleted (unset)

For these reasons (and more), tableau_tools has a whole lot of “knowledge” built in to translate what you see in Tableau Server UI into what the REST API requires, without you having to do too much work.

Basically, the Project, Workbook and Datasource classes store everything about the content that is published on the Tableau Server, including all the permissions. All changes to the permissions go through these classes, which allows them to use the most optimal algorithms for making the updates you want to send.

It also means that rather than just sending updates, the best pattern is:

  1. Retrieve the all existing permissions, which will come down as a List of Permissions objects
  2. Check to see if there is an existing Permissions object for the Group or User
    • If a Permissions object already exist:
      1. Make changes directly to that Permissions object
      2. Send the full original List of Permissions objects back to the Project, Workbook or Datasource object using the set_permissions_by_permissions_obj_list() method
    • If a Permissions object does not already exist:
      1. Use the factory methods on the Project class to generate the correct type of permissions objects. They are all named like “create_{content_type}_object_for_{group_or_user}(name_or_luid)” ex. perm_obj = create_workbook_permissions_object_for_group(group_luid)
      2. Set whichever capabilities you want. Ex. perm_obj.set_capability(u”Download Full Data”, u”Deny”)
      3. You can remove a capability completely (neither Allow or Deny) but using Permissions.set_capability_to_unspecified(u”Download Full Data”)
      4. Then send this Permissions object (or create multiple of them at once) and send using set_permissions_by_permissions_obj_list()
Advertisements

Leave a Reply

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

WordPress.com Logo

You are commenting using your WordPress.com 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