1
votes

I have a Xamarin Forms mobile client that I want to talk to Cosmosdb directly and I don't want to be dependent on - and have the overhead of - the entire DocumentDb SDK.

And since I'm on an untrusted client, I am using resource tokens for authentication. Everything is partitioned.

For testing purposes, I've replicated what I'm trying to do using both REST SQL and DocumentClient calls.

I have successfully retrieved a single document by issuing a Get call using REST and a resource token. This also worked just fine for a DocumentClient approach.

So far, so good.

When I try to actually do a query it works great using the DocumentClient and a resource token.

Using the exact same query and the exact same resource token with a REST call results in a Forbidden result.

The permission mode provided in the authorization token doesn't provide sufficient permissions

I read somewhere (and I can't find it now) that you need a master token to do queries using REST calls.

Before I post a bunch of code and write it up, I am experiencing the expected behavior or should I actually be able to query using REST calls?

Thanks in advance.

** UPDATE #2 WITH LINK TO GITHUB REPO**

https://github.com/nhwilly/DocumentClientVsRest.git

UPDATE WITH CODE SAMPLE

using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Newtonsoft.Json;

namespace DocClientVsRestCallTest
{
    /// <summary>
    /// The purpose of this console app is to determine why I can't get a REST call
    /// to work on a read only resource token for Azure CosmosDb.  A direct comparison
    /// using identical paths and tokens should work.  I have an issue for sure, but I
    /// can't find it.  :(
    ///
    /// To run this, you need to have already created a partitioned CosmosDb collection.
    /// </summary>
    class Program
    {
        public static string dbServicePath = $"https://[YOUR-COSMOS-ACCOUNT-NAME].documents.azure.com";
        public static string databaseId = "[YOUR-DATABASE-ID]";
        public static string collectionId = "[YOUR-COLLECTION-ID]";
        public static string datetime = DateTime.UtcNow.ToString("R");
        public static string version = "2018-06-18";
        public static string resourceId = $"dbs/{databaseId}/colls/{collectionId}";
        public static string urlPath = $"{dbServicePath}/{resourceId}/docs";
        public static string partitionKey = $"TestPartition";
        public static string documentId = $"TestDocumentId";
        public static string queryString = $"select * from c where c.id='{documentId}' and c.partitionId ='{partitionKey}'";
        public static string userId = $"TestUser";
        public static string permissionToken = string.Empty;

        // the master key is supplied to create permission tokens and simulate the server only.
        public static string masterKey = $"[YOUR-MASTER-KEY]";

        static void Main(string[] args)
        {
            Debug.WriteLine("Starting...");

            // let's make sure we get a readonly token for the user/partition in question.
            permissionToken =
                Task.Run(async () => await GetPermissionToken()).GetAwaiter().GetResult();


            QueryUsingSdk();

            Task.Run(async () => await QueryUsingRest()).ConfigureAwait(false);

            Task.Run(async ()=> await CleanUp()).ConfigureAwait(false);

            Console.WriteLine("finished...");
            Console.ReadKey();
        }

        static async Task QueryUsingRest()
        {
            Uri uri = new Uri(urlPath);
            HttpClient client = new HttpClient();

            var encodedToken =
                HttpUtility.UrlEncode(permissionToken);

            string partitionAsJsonArray =
                JsonConvert.SerializeObject(new[] { partitionKey });

            client.DefaultRequestHeaders.Add("x-ms-date", datetime);
            client.DefaultRequestHeaders.Add("x-ms-documentdb-isquery", "True");
            client.DefaultRequestHeaders.Add("x-ms-documentdb-query-enablecrosspartition", "False");
            client.DefaultRequestHeaders.Add("x-ms-documentdb-query-iscontinuationexpected", "False");
            client.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", partitionAsJsonArray);
            client.DefaultRequestHeaders.Add("authorization", encodedToken);
            client.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
            client.DefaultRequestHeaders.Add("x-ms-version", version);
            client.DefaultRequestHeaders.Accept
                .Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var content =
                new StringContent(JsonConvert.SerializeObject(new { query = queryString }), Encoding.UTF8, "application/query+json");

            HttpResponseMessage response =
               await client.PostAsync(urlPath, content).ConfigureAwait(false);

            if (!response.IsSuccessStatusCode)
            {
                await DisplayErrorMessage(response).ConfigureAwait(false);
            }
            else
            {
                Debug.WriteLine($"Success {response.StatusCode}!");
                var jsonString =
                    await response.Content.ReadAsStringAsync().ConfigureAwait(false);
            }

        }


        static void QueryUsingSdk()
        {

            var docClient =
                new DocumentClient(new Uri(dbServicePath), permissionToken);

            var feedOptions =
                new FeedOptions { PartitionKey = new PartitionKey(partitionKey) };

            var result =
                docClient
                    .CreateDocumentQuery(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId), queryString,
                        feedOptions)
                    .ToList().First();


            Debug.WriteLine($"SDK result: {result}");
        }


        /// <summary>
        /// This method simulates what would happen on the server during an authenticated
        /// request.  The token (and other permission info) would be returned to the client.
        /// </summary>
        /// <returns></returns>
        static async Task<string> GetPermissionToken()
        {
            string token = string.Empty;
            try
            {
                var docClient =
                    new DocumentClient(new Uri(dbServicePath), masterKey);

                var userUri =
                        UriFactory.CreateUserUri(databaseId, userId);

                // delete the user if it exists...
                try
                {
                    await docClient.DeleteUserAsync(userUri).ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Delete user error: {e.Message}");

                }

                // create the user
                var dbUri =
                    UriFactory.CreateDatabaseUri(databaseId);

                await docClient.CreateUserAsync(dbUri, new User { Id = userId }).ConfigureAwait(false);

                // create the permission
                var link =
                    await docClient
                        .ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId))
                        .ConfigureAwait(false);

                var resourceLink =
                    link.Resource.SelfLink;

                var permission =
                    new Permission
                    {
                        Id = partitionKey,
                        PermissionMode = PermissionMode.Read,
                        ResourceLink = resourceLink,
                        ResourcePartitionKey = new PartitionKey(partitionKey)
                    };


                await docClient.CreatePermissionAsync(userUri, permission).ConfigureAwait(false);

                // now create a document that should be returned when we do the query
                var doc = new { id = documentId, partitionId = partitionKey, message = "Sample document for testing" };
                try
                {
                    await docClient.DeleteDocumentAsync(UriFactory.CreateDocumentUri(databaseId, collectionId,
                        documentId), new RequestOptions { PartitionKey = new PartitionKey(partitionKey) }).ConfigureAwait(false);


                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Test document not found to delete - this is normal.");
                }

                try
                {
                    var document = await docClient
                        .CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(databaseId, collectionId), doc)
                        .ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    Debug.WriteLine($"Create document message: {e.Message}");
                }

                // now read the permission back as it would happen on the server
                var result = await docClient.ReadPermissionFeedAsync(userUri).ConfigureAwait(false);
                if (result.Count > 0)
                {
                    token = result.First(c => c.Id == partitionKey).Token;
                }


            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Create and get permission failed: {ex.Message}");
            }

            if (string.IsNullOrEmpty(token))
            {
                Debug.WriteLine("Did not find token");
            }
            return token;
        }

        static async Task CleanUp()
        {
            var docClient =
                new DocumentClient(new Uri(dbServicePath), masterKey);

            var doc = new { id = documentId, partitionId = partitionKey, message = "Sample document for testing" };
            try
            {
                await docClient.DeleteDocumentAsync(UriFactory.CreateDocumentUri(databaseId, collectionId,
                    documentId), new RequestOptions { PartitionKey = new PartitionKey(partitionKey) }).ConfigureAwait(false);


            }
            catch (Exception e)
            {
                Debug.WriteLine($"Delete document message: {e.Message}");
            }

        }

        static async Task DisplayErrorMessage(HttpResponseMessage response)
        {
            var messageDefinition =
                new
                {
                    code = "",
                    message = ""
                };

            var jsonString =
                await response.Content.ReadAsStringAsync().ConfigureAwait(false);

            var message =
                JsonConvert.DeserializeAnonymousType(jsonString, messageDefinition);

            Debug.WriteLine($"Failed with {response.StatusCode} : {message.message}");

        }

    }
}
1

1 Answers

0
votes

I am experiencing the expected behavior or should I actually be able to query using REST calls?

Yes,you could query documents in rest calls with resource token. Please refer to my sample rest java code as below :

import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONObject;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;

public class QueryDocumentsRest {

    private static final String account = "***";

    private static final String query = "select * from c";

    public static void main(String args[]) throws Exception {

        String urlString = "https://" + account + ".documents.azure.com/dbs/db/colls/coll/docs";
        HttpURLConnection connection = (HttpURLConnection) (new URL(urlString)).openConnection();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("query",query);
        JSONArray jsonArray = new JSONArray();
        jsonObject.put("parameters",jsonArray);
        byte[] data = (jsonObject.toString()).getBytes("UTF-8");

        connection.setRequestMethod("POST");
        connection.setRequestProperty("x-ms-version", "2017-02-22");
        connection.setRequestProperty("x-ms-documentdb-isquery", "true");
        //connection.setRequestProperty("x-ms-documentdb-query-enablecrosspartition", "true");

        connection.setRequestProperty("Content-Type", "application/query+json");
        System.out.println(data.length);
        connection.setRequestProperty("Content-Length", String.valueOf(data.length));

        SimpleDateFormat fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");
        fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
        String date = fmt.format(Calendar.getInstance().getTime()) + " GMT";
        String auth = getAuthenticationString();

        connection.setRequestProperty("x-ms-date", date);
        connection.setRequestProperty("Authorization", auth);

        connection.setDoOutput(true);
        OutputStream os = connection.getOutputStream();

        os.write(data);
        os.flush();
        os.close();

        System.out.println("Response message : " + connection.getResponseMessage());
        System.out.println("Response code : " + connection.getResponseCode());
        System.out.println(connection.getHeaderField("x-ms-request-charge"));


        BufferedReader br = null;
        if (connection.getResponseCode() != 200) {
            br = new BufferedReader(new InputStreamReader((connection.getErrorStream())));
        } else {
            br = new BufferedReader(new InputStreamReader((connection.getInputStream())));
        }
        System.out.println("Response body : " + br.readLine());
    }


    private static String getAuthenticationString() throws Exception {
        String auth = "type=resource&ver=1&sig=***";
        auth = URLEncoder.encode(auth);
        System.out.println("authString:" + auth);
        return auth;
    }

}

I set the permission mode as All during my test.