8 Replies Latest reply on Jul 3, 2017 10:50 AM by Brian Vinson

    Works with "Default" site, but not other sites???

    Jeremy Waters

      Hello All,

       

      I'm using Sparkler 1.0.3 with Tableau 9.3.1. I'm using the "trusted authentication" approach and have it working with dashboards in the "default" tableau site. But for the life of me, I can't get this to work with dashboards that are not in the default site - i.e. in a custom site we created.

       

      I keep getting "Could not locate unexpired trusted ticket" when using custom sites. I've enabled verbose logging in sparkler, and i'm seeing a couple issues. first, when sparkler requests a trusted ticket, it's not sending the siteId in the request. I see that being submitted with a null value. Second, when sparkler puts together the EmbedResponse, the tableau dashboard url is malformed. It has some extra encoding which butchers the ticket.

       

      sample:

      https://foobar.com/trusted/oqaDb1IXuKdmBATnDGA2-jow/t/MyCustomSite/views/MyDashboard/MyView?:embed=yes&:tabs=yes…

       

      ...where the siteRoot was set as "/t/MyCustomSite". The oddity is how the forward slashes are being encoded by sparkler as "/" resulting in an invalid URL.

       

      Does anyone have this working? with non-default site? and with trusted ticket authentication (not saml)?

       

      Thanks,

      Jeremy

        • 1. Re: Works with "Default" site, but not other sites???
          Jeff D

          Hi Jeremy, if Sparkler is requesting a trusted ticket to a non-default site without target_site=<site id> then it's not going to work.  You said that the site is being submitted as a null value.  Is there something unusual about the configuration for sparkler.tableau.trustedTicketSiteId ?

          • 2. Re: Works with "Default" site, but not other sites???
            Jeremy Waters

            Hi Jeff - I set sparkler.tableau.trustedTicketSiteId like this:

             

            <Environment name="sparkler.tableau.trustedTicketSiteId" value="MyCustomSite" type="java.lang.String" override="false" />

             

            And the sparkler log records:

             

            2016-09-19 11:04:21,404 DEBUG (http-nio-8080-exec-1) [com.tsi.bizsys.sparkler.domain.EmbedRequest.<init>:111] - sparkler.tableau.trustedTicketSiteId (MyCustomSite) is set in Sparkler configuration, use it as siteId to get trust ticket from Tableau Sever.

             

            2016-09-19 11:04:21,411 TRACE (http-nio-8080-exec-1) [com.tsi.bizsys.sparkler.services.EmbedServiceImpl.process:56] - EmbedRequest [javascriptLib=/javascripts/api/viz_v1.js, height=636, width=804, hostUrl=https://tableau.foobar.com/, siteRoot=/t/MyCustomSite, name=MyDash/TitlePage, showTabs=no, showToolbar=no, trustedTicketHost=tableau.foobar.com, signedTrustedTicketSiteId=null, signedTrustedTicketUsername=watersjeremy@praintl.com.test, filter=null, trustedTicket=null]

             

            2016-09-19 11:04:21,419 TRACE (http-nio-8080-exec-1) [com.tsi.bizsys.sparkler.client.AbstractTrustedTicketClient.getTrustedTicket:73] - TrustedTicketRequest [secure=true, host=tableau.foobar.com, port=443, siteId=null, username=watersjeremy@praintl.com.test]

             

            So the log confirms my trustedTicketSiteId setting is being read. But it also indicates that it's not being used to obtain a ticket...

             

            Thanks,

            Jeremy

            • 4. Re: Works with "Default" site, but not other sites???
              Martin Sleeman

              Hey Jeremy,

               

              In addition we've very recently released a new version of Sparkler that fixed an issue like this for multi-site Tableau Servers.  You can grab the latests download (v1.04) here.

               

              http://www.tableau.com/sfdc-canvas-adapter

              • 5. Re: Works with "Default" site, but not other sites???
                Jeremy Waters

                Hi Martin - I look forward to getting my hands on v1.04 and seeing if that resolves the issue. However, as of this morning (20-Sep-2016) that link is still serving v1.03. Has v1.04 been released/published yet?

                 

                Thanks,

                Jeremy

                • 6. Re: Works with "Default" site, but not other sites???
                  Brenda Finn

                  Jeremy et all

                   

                  I seem to be experiencing the same issue. Here is my situation.

                   

                  We have a Canvas App in Salesforce that is connecting to Sparkler -> Tableau.

                  We are  using signedIdentity and I have put the Apex code in place to send the username as standard_user1 per the documentation.

                   

                  Canvas App Preview works fine.

                  isAlive works

                  Status page works

                   

                  But when I try to display my VF page that contains the Canvas App, I am getting an Invalid Trusted Ticket. Looking in the logs, I see that the trustedTicket is null. According to the documentation here:

                   

                  sparkler.tableau.useTrustedTickets:

                   

                  A value that instructs Sparkler to perform trusted ticket requests against Tableau Server. If this value is true, the trusted ticket username must be passed in every embedded Salesforce Canvas App component.

                   

                   

                   

                  This means that I need to specify the username but how does that work if I am using signedIdentity? Why am I not getting a trusted ticket?

                   

                  Any help/insight would be appreciated.


                  Thank you in advance

                  Brenda

                  • 7. Re: Works with "Default" site, but not other sites???
                    Martin Sleeman

                    Hey Brenda,

                     

                    We've fixed this issue with Sparkler v1.0.4 which we have just posted as a download.  So click on Download the Adapter to get that version.

                     

                    Thanks!

                    -Martin Sleeman

                    Product Manager

                    • 8. Re: Works with "Default" site, but not other sites???
                      Brian Vinson

                      I am trying to setup Sparkler v1.04 to use multiple sites. For testing we have two sites with one workbook and one view each:

                      • SiteA/WorkbookA/ViewA
                      • SiteB/WorkbookB/ViewB

                       

                      I have followed the steps from the documentation and configured each item for multi-site using signedIdentity as instructed:

                      • TableauSparklerUtilities
                      • MyAccountController

                       

                      I have set the Sparkler.xml (see configuration for multisite below)

                       

                      I have created one VisualForce Pages for each test view:

                      • VFP: SiteA/WorkbookA/ViewA
                      • VFP: SiteB/WorkbookB/ViewB

                       

                      The default site in my Sparkler.xml is SiteA

                      • VFP: SiteA/WorkbookA/ViewA loads successfully
                      • VFP: SiteB/WorkbookB/ViewB thows an error, "Could not locate unexpired trusted ticket gs9LiIsmXXXiXXXXrtvJFMP8"

                       

                       

                      When sparkler requests a trusted ticket, it's not sending the siteId in the request. I see that being submitted with a null value. I've included all the code used for each of the components below. Do I have a parameter configured incorrectly? Am I missing something in the TableauSparklerUtilities to generate the correct signedIdentity? I am stumpped.

                       

                      --------------------------------------------------------------------------------------------------------------

                       

                      <!--TableauSparklerUtilities-->

                      /**

                      * Utility methods provided to Sparkler tenant developer

                      */

                      public class TableauSparklerUtilities {

                          /**

                          * TableauSparklerUtilities specific exception

                          */

                          public class TableauSparklerUtilitiesException extends Exception { }

                       

                       

                          private class UserIdentity {

                              String userName;

                              String siteId;

                              String timestamp;

                       

                       

                              UserIdentity(final String userName, final String siteId, final String timestamp) {

                                  this.userName = userName;

                                  this.siteId = siteId;

                                  this.timestamp = timestamp;

                              }

                          }

                       

                       

                          @TestVisible private static String serializeDateTimeToUtcFormat(final Datetime dt) {

                              return dt.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'UTC');

                          }

                       

                       

                          @TestVisible private static String serializeUserIdentityToJson(final String userName, final String siteId, final String timestamp) {

                              UserIdentity payLoad = new UserIdentity(userName, siteId, timestamp);

                              return JSON.serialize(payLoad);

                          }

                       

                       

                          @TestVisible private static String readTableauSparklerConsumerSecret(final String secretName) {

                              TableauSparklerConsumerSecret__c setting = TableauSparklerConsumerSecret__c.getInstance(secretName);

                       

                       

                              if (setting == null) {

                                  String msg = String.format('Cannot find record named as {0} in custom setting "TableauSparklerConsumerSecret".', new String[] {secretName});

                                  System.debug(msg);

                                  throw new TableauSparklerUtilitiesException(msg);

                              }

                       

                       

                              // SecretValue__c field is configured as a required text field, so its value won't be empty string

                              return setting.SecretValue__c;

                          }

                       

                       

                          @TestVisible private static String innerGenerateSignedIdentity(final String userName, final String siteId, final String timestamp, final String secret) {

                              if (String.isEmpty(userName)) {

                                  String msg = 'userName cannot be empty.';

                                  System.debug(msg);

                                  throw new TableauSparklerUtilitiesException(msg);

                              }

                       

                       

                              if (String.isEmpty(timestamp)) {

                                  String msg = 'timestamp cannot be empty.';

                                  System.debug(msg);

                                  throw new TableauSparklerUtilitiesException(msg);

                              }

                       

                       

                              if (String.isEmpty(secret)) {

                                  String msg = 'secret cannot be empty.';

                                  System.debug(msg);

                                  throw new TableauSparklerUtilitiesException(msg);

                              }

                       

                       

                              String jsonUserIdentity = TableauSparklerUtilities.serializeUserIdentityToJson(userName, siteId, timestamp);

                              String base64UserIdentity = EncodingUtil.base64Encode(Blob.valueOf(jsonUserIdentity));

                              Blob signature = crypto.generateMac('hmacSHA256', Blob.valueOf(base64UserIdentity), Blob.valueOf(secret));

                       

                       

                              return base64UserIdentity + '.' + EncodingUtil.base64Encode(signature);

                          }

                       

                       

                          private final static String defaultSecret = TableauSparklerUtilities.readTableauSparklerConsumerSecret('defaultSecret');

                       

                       

                          /**

                          * Generate a signed identity that is consist of a Tableau user name and a Tableau Server site id.

                          *

                          * @param userName The Tableau Server user name needed to be signed.

                          * @param siteId Tableau site ID needed to be signed.

                          * @return Signed identity information in base64 format.

                          */

                          public static String generateSignedIdentity(final String userName, final string siteId) {

                              // Datetime.now() returns the current Datetime based on a GMT calendar

                              // according to Salesforce Apex Code Developer's Guide (Version 35.0, Winter'16)

                              String nowUTC = TableauSparklerUtilities.serializeDateTimeToUtcFormat(Datetime.now());

                              return TableauSparklerUtilities.innerGenerateSignedIdentity(userName, siteId, nowUTC, defaultSecret);

                          }

                       

                       

                          /**

                          * Generate a signed identity that is consist of a Tableau user name.

                          *

                          * @param userName The Tableau Server user name needed to be signed.

                          * @return Signed identity information in base64 format.

                          */

                          public static String generateSignedIdentity(final String userName) {

                              return generateSignedIdentity(userName, null);

                          }

                      }

                       

                      --------------------------------------------------------------------------------------------------------------

                       

                      <!--MyAccountController-->

                      public class MyAccountController {

                          // Holds a reference to the account associated with the page controller

                          private final Account acct;

                       

                       

                          // Hold the username of a static Tableau username as a default

                          private static final String DEFAULT_TABLEAU_USERNAME = 'bvinson';

                       

                       

                          // The extension constructor initializes the private member

                          // variable acct by using the getRecord method from the standard

                          // controller.

                          public MyAccountController(ApexPages.StandardController stdController) {

                              this.acct = (Account)stdController.getRecord();

                              init();

                          }

                       

                       

                          //Initializes the controller.

                          public MyAccountController() {

                              this.acct = null;

                              init();

                          }

                       

                       

                          // Load the tableau username into a static at page load since this won't change

                          private void init() {

                          }

                       

                       

                          // This is the method that actually determines which field you want to use as the

                          // source of the mapping to a Tableau user

                          // In our example we use a static string "sfUser" as the string to map.

                          private String myCustomFieldMapper() {

                              // This customer written class does the "work" need to map and get a Tableau username

                              // In our example we use a static string "sfUser" as the string to map.

                              return DEFAULT_TABLEAU_USERNAME;

                          }

                       

                       

                          /**

                          * Gets the signed identity; always want to generate this in a getter since the constructor

                          * only gets called on original page load and timestamp will skew

                          */

                          public String getSignedIdentity() {

                              String signedIdentity = TableauSparklerUtilities.generateSignedIdentity(this.myCustomFieldMapper());

                              return signedIdentity;

                          }

                      }

                       

                      --------------------------------------------------------------------------------------------------------------

                       

                      <!--Sparkler.xml-->

                      <Environment name="sparkler.sfdc.userIdentifierField" value="signedIdentity" type="java.lang.String" override="false" />

                      <Environment name="sparkler.tableau.trustedTicketSiteId" value="SiteA" type="java.lang.String" override="false" />

                      <Environment name="sparkler.tableau.siteRoot" value="/t/" type="java.lang.String" override="false" />

                       

                      ---------------------------------------------------------------------------------------------------------------

                       

                      <!--VFP: SiteA/WorkbookA/ViewA-->

                      <apex:page standardController="Account" extensions="MyAccountController">

                      <apex:canvasApp applicationName="SparklerSandbox"

                          height="934px"

                          width="950px"

                          parameters="

                          {

                              'ts.trustedTicket.signedIdentity':'{!signedIdentity}',

                              'ts.siteRoot': '/t/SiteA',

                              'ts.name': 'WorkbookA/ViewA',

                              'ts.filter': 'UserTableau={!$User.Email}',

                          }" />

                      </apex:page>

                       

                      *********************************************************************************************************

                      <!--Sparkler Log SiteA/WorkbookA/ViewA-->

                      [com.tsi.bizsys.sparkler.domain.EmbedRequest.<init>:111] - sparkler.tableau.trustedTicketSiteId (SiteA) is set in Sparkler configuration, use it as siteId to get trust ticket from Tableau Sever.

                       

                       

                      [com.tsi.bizsys.sparkler.services.EmbedServiceImpl.process:56] - EmbedRequest [javascriptLib=/javascripts/api/viz_v1.js, height=934, width=950, hostUrl=https://reports.company.com/, siteRoot=/t/SiteA, name=WorkbookA/ViewA, showTabs=no, showToolbar=no, trustedTicketHost=reports.company.com, signedTrustedTicketSiteId=SiteA, signedTrustedTicketUsername=bvinson, filter=UserTableau=bvinson@company.com, trustedTicket=null]

                       

                       

                      [com.tsi.bizsys.sparkler.client.AbstractTrustedTicketClient.getTrustedTicket:73] - TrustedTicketRequest [secure=true, host=reports.company.com, port=443, siteId=SiteA, username=bvinson]

                       

                       

                      [org.apache.http.wire.wire:86] - http-outgoing-4 << "e4XXXeP5nYwg7loNsIzgHsGP"

                       

                       

                      document.getElementById(placeHolderName).src = "https://reports.company.com/trusted/e4XXXeP5nYwg7loNsIzgHsGP/t/SiteA/views/WorkbookA/ViewA?:embed=yes&:tabs=no&:toolbar=no&UserTableau=bvinson@company.com";

                       

                       

                      BROWSER RESULTS: Successfully embeds WorkbookA/ViewA

                       

                      ----------------------------------------------------------------------------------------------------------

                       

                      <!--VFP: SiteB/WorkbookB/ViewB-->

                      <apex:page standardController="Account" extensions="MyAccountController">

                      <apex:canvasApp applicationName="SparklerSandbox"

                          height="934px"

                          width="950px"

                          parameters="

                          {

                              'ts.trustedTicket.signedIdentity':'{!signedIdentity}',

                              'ts.siteRoot': '/t/SiteB',

                              'ts.name': 'WorkbookB/ViewB',

                              'ts.filter': 'UserTableau={!$User.Email}',

                          }" />

                      </apex:page>

                       

                       

                      *********************************************************************************************************

                      <!--Sparkler Log SiteB/WorkbookB/ViewB-->

                      [com.tsi.bizsys.sparkler.domain.EmbedRequest.<init>:111] - sparkler.tableau.trustedTicketSiteId (SiteA) is set in Sparkler configuration, use it as siteId to get trust ticket from Tableau Sever.

                       

                       

                      [com.tsi.bizsys.sparkler.services.EmbedServiceImpl.process:56] - EmbedRequest [javascriptLib=/javascripts/api/viz_v1.js, height=934, width=804, hostUrl=https://reports.entravision.com/, siteRoot=/t/SiteB, name=Test/Test, showTabs=no, showToolbar=no, trustedTicketHost=reports.company.com, signedTrustedTicketSiteId=SiteA, signedTrustedTicketUsername=bvinson, filter=null, trustedTicket=null]

                       

                       

                      [com.tsi.bizsys.sparkler.client.AbstractTrustedTicketClient.getTrustedTicket:73] - TrustedTicketRequest [secure=true, host=reports.company.com, port=443, siteId=SiteA, username=bvinson]

                       

                       

                      [org.apache.http.wire.wire:86] - http-outgoing-0 >> "username=bvinson&target_site=SiteA"

                       

                       

                      document.getElementById(placeHolderName).src = "https://reports.company.com/trusted/gs9LiIsmXXXiXXXXrtvJFMP8/t/SiteB/views/WorkbookB/ViewB?:embed=yes&:tabs=no&:toolbar=no";

                       

                       

                      BROWSER RESULTS: Error: Could not locate unexpired trusted ticket gs9LiIsmXXXiXXXXrtvJFMP8