8 Replies Latest reply on Apr 23, 2018 1:22 PM by Robin Cottiss

    Unable to create user using REST API in C# code (401 Unauthorized Error)

    Binit Kumar

      I am trying to implement 'Managing Users' on Tableau server 10.5 using C# code.

       

      • Below code is able to successfully do the below 3 actions:
        1. Login into Tableau server 10.5 site and retrieve a two part authentication token.
        2. Get a list of all users on the Tableau website.
        3. Delete a user on the Tableau website
      • Below code isn't able to do the following 1 action:
        1. Add a new user to the Tableau website. (I receive a 401 Unauthorized error message)

       

      • NOTE: Here are a few things I have double checked:
        1. The REST API access is enabled on the server. That is how I am able to make all the other calls from this code.
        2. The create user call to the same server/ website works fine using POSTMAN.
        3. I have made sure that the request XML sent contains the appropriate <tsRequest> and <user> with name and siteRole Parameters.

       

      Here is the code I am working with:

       

      static string LoginToTableau()

              {

                  //Create XML payload for the api call. 

                  using (XmlWriter loginxml = XmlWriter.Create("login.xml"))

                  {

                      loginxml.WriteStartDocument();

                      loginxml.WriteStartElement("tsRequest");

                      loginxml.WriteStartElement("credentials");

                      loginxml.WriteAttributeString("name", <Username>");

                      loginxml.WriteAttributeString("password", "<Password>");

                      loginxml.WriteStartElement("site");

                      loginxml.WriteAttributeString("contentUrl", "");

                      loginxml.WriteEndElement();

                      loginxml.WriteEndElement();

                      loginxml.WriteEndElement();

                      loginxml.WriteEndDocument();

                  }

                  XElement myxml = XElement.Load("login.xml");

       

       

                  //Convert the XML payload to a string and display so we can check that it's well-formed 

                  string myxmlstring = myxml.ToString();           

       

       

                  string tableauUrl = "https://<TableauServerName>/api/2.3/auth/signin";

                  var tableauInfo = SendWebRequest(tableauUrl, "POST", myxmlstring, null);

                  return tableauInfo;

              }       

       

       

              static string SendWebRequest(string Url, string Method, string payload, string headers)

              {

                  string response;

                

                  //Create the web request and add the XML payload 

                  HttpWebRequest wc = WebRequest.Create(Url) as HttpWebRequest;

                  wc.Method = Method;

                 

                  //encode the XML payload

                  if (payload != null)

                  {

                      wc.ContentType = "application/xml";

                      wc.Accept = "application/xml";

                      byte[] buf = Encoding.UTF8.GetBytes(payload);

                      wc.ContentLength = buf.Length;

                      wc.GetRequestStream().Write(buf, 0, buf.Length);

                  }          

                  if (headers != null)

                  {

                      wc.Headers.Add("X-Tableau-Auth: "+ _token);

                  }          

       

       

                  try

                  {

                      //Send the web request and parse the response into a string

       

       

                      HttpWebResponse wr = wc.GetResponse() as HttpWebResponse;  

                       // Above line is where I receive the error 401 Unauthorized only while making the POST create user call. The POST login call, GET users call and DELETE user call work fine.

       

                

                      Stream receiveStream = wr.GetResponseStream();

                      StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8);

                      response = readStream.ReadToEnd();

       

       

                      receiveStream.Close();

                      readStream.Close();

                      wr.Close();

       

       

       

       

                      readStream.Dispose();

                      receiveStream.Dispose();

                  }

                  catch (WebException we)

                  {

                      //Catch failed request and return the response code 

                      response = ((HttpWebResponse)we.Response).StatusCode.ToString();

                  }

                  return response;

              }      

       

       

              protected void BindUsersGrid(){

       

       

                  string tableauUrl = "https://<TableauServerName>/api/2.8/sites/" + _siteId + "/users";

                  var userResponse = SendWebRequest(tableauUrl, "GET", null, _token);

       

       

                  DataTable dt = new DataTable();

                  dt.Columns.AddRange(new DataColumn[3]{

                          new DataColumn("Name", typeof(string)),

                          new DataColumn("Role", typeof(string)),

                          new DataColumn("Id", typeof(string))

                      });

       

       

                  XmlDocument userDoc = new XmlDocument();

                  userDoc.LoadXml(userResponse);

       

       

                  foreach (XmlNode node in userDoc.GetElementsByTagName("user"))

                  {

                      string userName = node.Attributes["name"].Value;

                      string siteRole = node.Attributes["siteRole"].Value;

                      string userId = node.Attributes["id"].Value;

                      dt.Rows.Add(userName, siteRole, userId);

                  }

       

       

                  UserList.DataSource = dt;

                  UserList.DataBind();

              }

                    

              protected void GetUsersBtn_Click(object sender, EventArgs e)

              {

                  BindUsersGrid();

              }       

       

       

              protected void CreateUserBtn_Click(object sender, EventArgs e)

              {

                  string newUser = createUserTxt.Text;

                  string newUserRole = userRoleDdl.SelectedItem.Text;

       

       

                  //Create XML payload for the api call. 

                  using (XmlWriter loginxml = XmlWriter.Create("addUser.xml"))

                  {

                      loginxml.WriteStartDocument();

                      loginxml.WriteStartElement("tsRequest");

                      loginxml.WriteStartElement("user");

                      loginxml.WriteAttributeString("name", newUser);

                      loginxml.WriteAttributeString("siteRole", newUserRole);

                      loginxml.WriteEndElement();

                      loginxml.WriteEndElement();

                      loginxml.WriteEndDocument();

                  }

                  XElement userxml = XElement.Load("addUser.xml");

       

       

                  //Convert the XML payload to a string and display so we can check that it's well-formed 

                  string createUserXmlstring =userxml.ToString();

       

       

                  string tableauUrl = "https://<TableauServerName>/api/2.8/sites/" + _siteId + "/users";

                  var createUserResponse = SendWebRequest(tableauUrl, "POST", createUserXmlstring, _token); // This is where I call the SendWebRequest function and pass the parameters

              }

       

       

              protected void UserList_RowCommand(object sender, GridViewCommandEventArgs e)

              {

                  int i = Convert.ToInt32(e.CommandArgument);

                  GridViewRow r = UserList.Rows[i];

                  string userId = r.Cells[2].Text;

       

       

                  string tableauUrl = "https://<TableauServerName>/api/2.8/sites/" + _siteId + "/users/"+ userId;

                  var deleteUserResponse = SendWebRequest(tableauUrl, "DELETE", null, _token);          

              }

       

       

      Thank you for your help and insights.

      Please let me know if you have any follow up questions that may help you answer my question better.

        • 1. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
          Robin Cottiss

          You seem to be calling the list users endpoint not an add user endpoint. Check the url you are using.

          • 2. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
            Binit Kumar

            Thank you for the response Robin. However, I am curious about what makes you say that.

             

            The endpoint to list users and create a new user is the same. The difference as I understand is that one of them doesn't contain a payload (list users) and the other one (create user) does.

             

            Here is the Tableau documentation:

            Get List of users- Reference-Tableau Server REST API 

            Create user- Reference-Tableau Server REST API

             

            Also, I use the same endpoint to create a user in Postman and it works fine.

            2018-04-22_8-52-32.png

            • 3. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
              Robin Cottiss

              You are correct. My mistake. Kudos to you for a model initial post BTW and anti-kudos to me for trying to answer a question on my phone before coffee

               

              I forgot they are the same endpoint.

               

              The 401 implies not authenticated (Handling Errors-Tableau Server REST API ) that the C# code is not picking up the authentication token or the user you are using is not authorized to create the user. Both scenarios seem unlikely as you are doing all the right things to test and compare your code to postman etc. but this is the implication from the 401 alone.

               

              Can you see the detailed 6 digit response code? it will be 401xxx. Also if you have access to the Tableau Server logs at the server side can be helpful.

               

              Do you have a C# example of a working call. I know they are not apples to apples but seeing you working code might help us see a difference.

              • 4. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
                Robin Cottiss

                Now I am on a PC I can read the code a bit better. It's been a while since I did any C# but how does your method to create the user get the value _token you pass it in but the method calli it header then you refer to _token. Also I am not seeing the call to login so there must be other code and perhaps global variables. Is there some sort ot state management going on that we are not seeing in this code?

                 

                Is this a console app or a web app?

                 

                Robin

                • 5. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
                  Binit Kumar

                  Hi Robin Cottiss ,

                   

                  Thank you for following up on this.

                   

                  Here are the answers to your questions:

                  1. Is this a console or a web app? Web app
                  2. I create two global variables _token and _site that save the two part token and website GUID values.
                  3. My app upon initial page load calls the sign-in functionality and saves the values in point 2. I then use these 2 values in all subsequent calls like GetUsersBtn_Click and CreateUserBtn_Click functions above.

                   

                  I acknowledge your concern about session management. However, if that was the case then I wouldn't have been able to make any subsequent calls. Also, the default token setting on my tableau server is 120 minutes so it doesn't make sense that it would be invalid.

                   

                  Also, I have made sure that the token value is actually passed into the GetWebRequest function and the HttpWebRequest is created and sent with the correct URI, method and xml values.

                  • 6. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
                    Robin Cottiss

                    Hi Binit Kumar I was not thinking about the lifetime of the token on the tableau server but how you access it on your web server. As I said, its a long time since I did this sort of coding in C# but I am now suspecting a threading issue. How exactly to you set your global variable? Best practice used to be use a thread safe singleton pattern. I am not sure how your code is guaranteeing that _token is the value you think it is at runtime.

                     

                    C# in Depth: Implementing the Singleton Pattern

                    • 7. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
                      Binit Kumar

                      Hi Robin Cottiss,

                       

                      I solved the issue finally.

                      It didn't really have much to do with the session state/ or threading.

                       

                      All I did was flip the order in which the 'X-Tableau-Auth' header and the xml payload were being added to the request.

                      Here's what the code looks like now:

                       

                      if (headers != null)

                                  {

                                      wc.Headers.Add("X-Tableau-Auth: " + headers); // Adding the header before before encoding the xml payload makes the call work.

                                  }

                       

                       

                                  //encode the XML payload

                                  if (payload != null)

                                  {

                                      wc.ContentType = "application/xml";

                                      wc.Accept = "application/xml";

                                      byte[] buf = Encoding.UTF8.GetBytes(payload);

                                      wc.ContentLength = buf.Length;

                                      wc.GetRequestStream().Write(buf, 0, buf.Length);

                                  }

                       

                      P.S.- I now understand how the initial login call, get users call and delete user call were working with the same order.

                      • Initial Login call- Doesn't have 'X-Tableau-Auth' header
                      • Get Users and Delete user- Don't have a payload

                       

                      In conclusion: If a Tableau call has both headers and xml payload contained in it, make sure you add the header to the call before the xml payload and convert it to byte string.

                       

                      Thank you again for your help through this. It has been an insightful discussion.

                      • 8. Re: Unable to create user using REST API in C# code (401 Unauthorized Error)
                        Robin Cottiss

                        Thanks Binit for sharing your success. This now makes sense. You are creating a stream or sequence of bytes representing the whole message and headers need to come first in the whole message followed by the content payload.

                         

                        Sorry I was not able to help but I would consider your original message a model for others seeking assistance from the community.

                         

                        Robin