On to problem 3 in my series on customizing the Tableau server experience...
Problem #3 - Lack of Access Transparency
Another problem I've encountered is lack of transparency into who has access to reports on Tableau Server. At Piedmont, we manage rights based on user groups. User groups are assigned to project folders. When a workbook is published to a project, it inherits the rights of the project. But end users cannot see the users that exist in these groups. In version 8.1, Tableau added the ability to expand a user group to see the users when publishing a workbook to the server, but report consumers still do not have the ability to see user rights on Server.
I'm hoping at some point this is solved natively in Tableau Server. But one of the great things about Tableau is it's flexibility. That usually translates into the ability to do pretty much anything you want if you understand how Tableau works. The secret sauce to the solution for this problem relies on Tableau features that are available through tabcmd and URL filtering, and sprinkles in a little scripting code. I used Ruby, but you could use Python or any other language you are familiar with. This is the end result:
Step 1 - Extract the members of your Tableau user groups into XML files
Start by opening notepad and creating a batch file. The key commands are: "tabcmd login", "tabcmd get", and "tabcmd logout". For help on each of these commands, see: http://onlinehelp.tableausoftware.com/current/server/en-us/tabcmd_cmd.htmYou will create a "tabcmd get" line for each user group in your server instance. There are some important parts to note when constructing the strings. Each get commands will be proceeded by "/users.xml?fe_group=". If the group is local, you will then add "local%%5C" followed by the group name. If the group is from Active Directory, you won't need to add this. Next enter the group name. If you have spaces in your group name, replace each space with "%%20". The "%%XX" parts are URL encodings. "%%5C" converts to a "\" and "%%20" converts to a space. If you are typing these into the command line, you don't need the encoded values, but for some reason you do when using batch files. The next segment of your string is "--filename" followed by the path and file name where you want to extract the group members. The format is XML. Finally, if you are running SSL, you will need to add "--no-certcheck" to the end of each command. Here is an example batch file for extracting the local group, "HR Analytics Users":
tabcmd login [your site credentials] --no-certcheck
tabcmd get "/users.xml?fe_group=local%%5CHR%%20Analytics%%20Users" --filename "\\phcms01\Share_Data\Tableau Data\user_audit_HR Analytics Users.xml" --no-certcheck
tabcmd get [additional group 2]
tabcmd logout --no-certcheck
Once you have your file with all your groups, save the it with the .bat extension. Next you will use Windows Task Scheduler to execute the .bat file every day. Do this on your primary server (unless you have enabled remote command administration).
Step 2 - Consolidate the results of your XML files into a single CSV file
This part requires a little scripting. Please note that I'm not an expert with Ruby, but I have just enough of a programming background to figure this stuff out. So I'm not the person to ask for help troubleshooting scripting code. Also, explaining the Ruby code is beyond the scope of this post. With that said, this is the code I use in Ruby to parse the XML files and insert into a single CSV file:
require 'net/http'
require 'rexml/document'
require 'csv'
groups = [ '0PHC Corp Directors',
'0PHC Corp Execs',
'0PHC Corp Managers',
'2PAH Directors',
'2PAH Execs',
'2PAH Managers',
'3PFH Directors',
'3PFH Execs',
'3PFH Managers',
'4PMH Directors',
'4PMH Execs',
'4PMH Managers',
'5PNH Directors',
'5PNH Execs',
'5PNH Managers',
'6PHI Directors',
'6PHI Execs',
'6PHI Managers',
'6PHI Physicians',
'7PMCC Directors',
'7PMCC Execs',
'7PMCC Managers',
'7PMCC Physicians',
'Corporate Directors',
'Corporate Finance',
'Corporate PMO',
'CSS Directors',
'CV Services Administration',
'DCIA - Desktop Clinical Intelligence Analysts',
'Epic Report Writers',
'Executive Group',
'Financial Analysts',
'Financial Planning & Analysis',
'HR Analytics Content Admins',
'HR Analytics Users',
'Labor and Productivity',
'March Madness Players',
'Marketing',
'Medical Records',
'Patient Financial Services',
'Patient Financial Services - Content Admins',
'Physician Outreach',
'Piedmont Atlanta Administration',
'Piedmont Atlanta Content Admins',
'Piedmont Fayette Administration',
'Piedmont Fayette Content Admins',
'Piedmont Heart Administration',
'Piedmont Heart Content Admins',
'Piedmont Henry Administration',
'Piedmont Henry Content Admins',
'Piedmont Mountainside Administration',
'Piedmont Mountainside Content Admins',
'Piedmont Newnan Administration',
'Piedmont Newnan Content Admins',
'PMCC Administration',
'PMCC Content Admin',
'Revenue Cycle Administration Viewers',
'Revenue Cycle Content Admins',
'Revenue Cycle Management',
'SCIA - Server Clinical Intelligence Analysts',
'Piedmont Information Systems',
'0PHC Systemwide Employees',
'Tableau_All'
]
directorplus = [ '0PHC Corp Directors',
'0PHC Corp Execs',
'2PAH Directors',
'2PAH Execs',
'3PFH Directors',
'3PFH Execs',
'4PMH Directors',
'4PMH Execs',
'5PNH Directors',
'5PNH Execs',
'6PHI Directors',
'6PHI Execs',
'7PMCC Directors',
'7PMCC Execs',
'Executive Group'
]
managerplus = [ '0PHC Corp Directors',
'0PHC Corp Execs',
'2PAH Directors',
'2PAH Execs',
'3PFH Directors',
'3PFH Execs',
'4PMH Directors',
'4PMH Execs',
'5PNH Directors',
'5PNH Execs',
'6PHI Directors',
'6PHI Execs',
'7PMCC Directors',
'7PMCC Execs',
'Executive Group',
'2PAH Managers',
'3PFH Managers',
'4PMH Managers',
'5PNH Managers',
'6PHI Managers',
'7PMCC Managers'
]
pahmanagerplus = [ '2PAH Directors',
'2PAH Execs',
'2PAH Managers'
]
file = '//phcms01/Share_Data/Tableau Data/user_audit.csv'
fileExists = File.exist?(file)
if fileExists
File.delete(file)
end
CSV.open(file,"a") do |csv|
csv << %w{Group User_ID User_Name Email}
groups.each do |group|
# get the XML data as a string
xml_data = File.new("//phcms01/Share_Data/Tableau Data/user_audit_#{group}.xml")
# extract event information
doc = REXML::Document.new(xml_data)
doc.elements.each('users/user') do |ele|
print "#{group} | #{ele.elements['name'].text} | #{ele.elements['friendly_name'].text} | #{ele.elements['email'].text}\n"
csv << [
"#{group}",
"#{ele.elements['name'].text}",
"#{ele.elements['friendly_name'].text}",
"#{ele.elements['email'].text}"
]
end
end
directorplus.each do |group|
# get the XML data as a string
xml_data = File.new("//phcms01/Share_Data/Tableau Data/user_audit_#{group}.xml")
# extract event information
doc = REXML::Document.new(xml_data)
doc.elements.each('users/user') do |ele|
print "DirectorPlus | #{ele.elements['name'].text} | #{ele.elements['friendly_name'].text} | #{ele.elements['email'].text}\n"
csv << [
"DirectorPlus",
"#{ele.elements['name'].text}",
"#{ele.elements['friendly_name'].text}",
"#{ele.elements['email'].text}"
]
end
end
managerplus.each do |group|
# get the XML data as a string
xml_data = File.new("//phcms01/Share_Data/Tableau Data/user_audit_#{group}.xml")
# extract event information
doc = REXML::Document.new(xml_data)
doc.elements.each('users/user') do |ele|
print "ManagerPlus | #{ele.elements['name'].text} | #{ele.elements['friendly_name'].text} | #{ele.elements['email'].text}\n"
csv << [
"ManagerPlus",
"#{ele.elements['name'].text}",
"#{ele.elements['friendly_name'].text}",
"#{ele.elements['email'].text}"
]
end
end
pahmanagerplus.each do |group|
# get the XML data as a string
xml_data = File.new("//phcms01/Share_Data/Tableau Data/user_audit_#{group}.xml")
# extract event information
doc = REXML::Document.new(xml_data)
doc.elements.each('users/user') do |ele|
print "PAHManagerPlus | #{ele.elements['name'].text} | #{ele.elements['friendly_name'].text} | #{ele.elements['email'].text}\n"
csv << [
"PAHManagerPlus",
"#{ele.elements['name'].text}",
"#{ele.elements['friendly_name'].text}",
"#{ele.elements['email'].text}"
]
end
end
end
Step 3 - Build a view in Tableau that leverages the CSV file created in step 2
Next, create a Tableau workbook that attaches to the resulting CSV file. It could just be a simple cross-tab with the User Name field on the row shelf. Publish the resulting view to the server. We will send the group filter in the URL.
Step 4 - Create project descriptions with links to the view created in step 3
The final step is to customize the project descriptions and provide the end user with links to see who is in each group that has rights to the project folder. The setup looks like this:The end result looks like this:
Conclusion
This wraps up my series on customizing the Tableau Server experience. I hope what you've learned is that Tableau is a very flexible product that you can customize to meet your needs. In my experience, you can typically overcome a lot of the limitations you encounter if you are creative and understand how a lot of the features of Tableau work. In many ways it is a simple, but also very deep product. If you missed any of the previous posts, you can find them here:
Part 1 (Landing Page): http://ugamarkj.blogspot.com/2013/11/customizing-tableau-server-experience.html
Part 1.5 (Report Portal): http://ugamarkj.blogspot.com/2013/12/customizing-tableau-server-experience.html
Part 2 (Integrated User Instruction): http://ugamarkj.blogspot.com/2013/12/customizing-tableau-server-experience_15.html