3

I'm trying to connect to a Gremlin collection in Azure Cosmos DB using a resource token. I adapted the documentation from here (it's for C# mainly): https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-use-resource-tokens-gremlin

The issue is that the date header of the token seems to be invalid once I try to access the data:

Exception in thread "main" java.util.concurrent.CompletionException: org.apache.tinkerpop.gremlin.driver.exception.ResponseException: 

ActivityId : 00000000-0000-0000-0000-000000000000
ExceptionType : UnauthorizedException
ExceptionMessage :
    The input date header is invalid format. Please pass in RFC 1123 style date format.
    ActivityId: 755ab024-fc79-47a3-bc44-3231b2db7dc1, documentdb-dotnet-sdk/2.7.0 Host/64-bit MicrosoftWindowsNT/6.2.9200.0
Source : Microsoft.Azure.Documents.ClientThe input date header is invalid format. Please pass in RFC 1123 style date format.
ActivityId: 755ab024-fc79-47a3-bc44-3231b2db7dc1, documentdb-dotnet-sdk/2.7.0 Host/64-bit MicrosoftWindowsNT/6.2.9200.0
    BackendStatusCode : Unauthorized
    BackendActivityId : 755ab024-fc79-47a3-bc44-3231b2db7dc1
    HResult : 0x80131500

Anyone knows how to fix that? The JVM is set to GMT via -Duser.timezone=GMT

Here's the code. Please note that it's a Java CLI application just for testing connectivity. All data of the cfg is basically given by cli, method names should be self-explanatory.

Token generation, this is using the master key for the DocumentClient instance:

...
import com.microsoft.azure.documentdb.DocumentClient;
import com.microsoft.azure.documentdb.DocumentClientException;
import com.microsoft.azure.documentdb.FeedResponse;
import com.microsoft.azure.documentdb.Permission;
import com.microsoft.azure.documentdb.PermissionMode;
import com.microsoft.azure.documentdb.ResourceResponse;
import com.microsoft.azure.documentdb.User;
...

public class TokenGenerator {

    private String USER_ID = "demo-1";

    public String generateToken(CmdLineConfiguration cfg) throws DocumentClientException {
        try (DocumentClient client = Utilities.documentClientFrom(cfg)) {
            String databaseLink = String.format("/dbs/%s", cfg.getDatabaseId());
            String collectionLink = String.format("/dbs/%s/colls/%s", cfg.getDatabaseId(), cfg.getCollectionId());

            // get all users within database
            FeedResponse<User> queryResults = client.readUsers(databaseLink, null);
            List<User> onlineUsers = queryResults.getQueryIterable().toList();

            // if a user exists, grab the first one, if not create it
            User user;
            Optional<User> onlineUser = onlineUsers.stream().filter(u -> u.getId().equals(USER_ID)).findFirst();
            if (onlineUser.isPresent()) {
                user = onlineUser.get();
            } else {
                User u = new User();
                u.setId(USER_ID);
                ResourceResponse<User> generatedUser = client.createUser(databaseLink, u, null);
                user = generatedUser.getResource();
            }

            // read permissions, if existent use, else create
            FeedResponse<Permission> permissionResponse = client.readPermissions(user.getSelfLink(), null);
            List<Permission> onlinePermissions = permissionResponse.getQueryIterable().toList();
            Permission permission;
            if (onlinePermissions.size() == 0) {
                Permission p = new Permission();
                p.setPermissionMode(PermissionMode.Read);
                p.setId(USER_ID + "_READ");
                p.setResourceLink(collectionLink);
                ResourceResponse<Permission> generatedPermission = client.createPermission(user.getSelfLink(), p, null);
                permission = generatedPermission.getResource();
            } else {
                permission = onlinePermissions.get(0);
            }
            // return the token
            return permission.getToken();
        }
    }
}

Connect and query Gremlin:

...
import org.apache.tinkerpop.gremlin.driver.AuthProperties;
import org.apache.tinkerpop.gremlin.driver.AuthProperties.Property;
import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.Cluster;
import org.apache.tinkerpop.gremlin.driver.ResultSet;
...


        Cluster cluster;
        String collectionLink = String.format("/dbs/%s/colls/%s", cfg.getDatabaseId(), cfg.getCollectionId());
        TokenGenerator tg = new TokenGenerator();
        String token = tg.generateToken(cfg);

        Cluster.Builder builder = Cluster.build(new File("src/remote.yaml"));
        AuthProperties authenticationProperties = new AuthProperties();
        authenticationProperties.with(AuthProperties.Property.USERNAME, collectionLink);

        authenticationProperties.with(Property.PASSWORD, token);
        builder.authProperties(authenticationProperties);
        cluster = builder.create();

        Client client = cluster.connect();
        ResultSet results = client.submit("g.V().limit(1)");

        // the following call fails
        results.stream().forEach(System.out::println);

        client.close();
        cluster.close();
    }

src/remote.yml

hosts: [COSMOSNAME.gremlin.cosmosdb.azure.com]
port: 443
username: /dbs/DBNAME/colls/COLLECTIONNAME
connectionPool: { enableSsl: true}
serializer: { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0, config: { serializeResultToString: true }}
dArignac
  • 1,205
  • 4
  • 11
  • 25

2 Answers2

0

I can't reproduce your issue on my side. I tried to extend this cosmos graph demo for java.In that demo,master key is used,so i followed your partial code and official sample to access graph db with resource token.

Your code of connection with db seems working.My main class as below which is similar to yours:

import org.apache.tinkerpop.gremlin.driver.*;
import org.apache.tinkerpop.gremlin.driver.exception.ResponseException;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;


public class Program
{
        static final String gremlinQueries[] = new String[] {"g.V().limit(1)"};

  public static void main( String[] args ) throws ExecutionException, InterruptedException {

        String token = "***";
        Cluster cluster;
        Client client;

        try {
            Cluster.Builder builder = Cluster.build(new File("src/remote.yaml"));
//            Cluster.Builder builder = Cluster.build();
            AuthProperties authenticationProperties = new AuthProperties();
            authenticationProperties.with(AuthProperties.Property.USERNAME,
                    String.format("/dbs/%s/colls/%s", "db", "coll"));

// The format of the token is "type=resource&ver=1&sig=<base64 string>;<base64 string>;".
            authenticationProperties.with(AuthProperties.Property.PASSWORD, token);

            builder.authProperties(authenticationProperties);

            // Attempt to create the connection objects
            cluster = builder.create();
//            cluster = Cluster.build(new File("src/remote.yaml")).create();
            client = cluster.connect();
        } catch (Exception e) {
            // Handle file errors.
            System.out.println("Couldn't find the configuration file.");
            e.printStackTrace();
            return;
        }

        // After connection is successful, run all the queries against the server.
        for (String query : gremlinQueries) {
            System.out.println("\nSubmitting this Gremlin query: " + query);

            // Submitting remote query to the server.
            ResultSet results = client.submit(query);

            CompletableFuture<List<Result>> completableFutureResults;
            CompletableFuture<Map<String, Object>> completableFutureStatusAttributes;
            List<Result> resultList;
            Map<String, Object> statusAttributes;

            try{
                completableFutureResults = results.all();
                completableFutureStatusAttributes = results.statusAttributes();
                resultList = completableFutureResults.get();
                statusAttributes = completableFutureStatusAttributes.get();            
            }
            catch(ExecutionException | InterruptedException e){
                e.printStackTrace();
                break;
            }
            catch(Exception e){
                ResponseException re = (ResponseException) e.getCause();

                // Response status codes. You can catch the 429 status code response and work on retry logic.
                System.out.println("Status code: " + re.getStatusAttributes().get().get("x-ms-status-code")); 
                System.out.println("Substatus code: " + re.getStatusAttributes().get().get("x-ms-substatus-code")); 

                // If error code is 429, this value will inform how many milliseconds you need to wait before retrying.
                System.out.println("Retry after (ms): " + re.getStatusAttributes().get().get("x-ms-retry-after"));

                // Total Request Units (RUs) charged for the operation, upon failure.
                System.out.println("Request charge: " + re.getStatusAttributes().get().get("x-ms-total-request-charge"));

                // ActivityId for server-side debugging
                System.out.println("ActivityId: " + re.getStatusAttributes().get().get("x-ms-activity-id"));
                throw(e);
            }

            for (Result result : resultList) {
                System.out.println("\nQuery result:");
                System.out.println(result.toString());
            }

            // Status code for successful query. Usually HTTP 200.
            System.out.println("Status: " + statusAttributes.get("x-ms-status-code").toString());

            // Total Request Units (RUs) charged for the operation, after a successful run.
            System.out.println("Total charge: " + statusAttributes.get("x-ms-total-request-charge").toString());
        }

        System.out.println("Demo complete!\n Press Enter key to continue...");
        try{
            System.in.read();
        } catch (IOException e){
            e.printStackTrace();
            return;
        }

        // Properly close all opened clients and the cluster
        cluster.close();

        System.exit(0);
    }
}

The yaml file:

hosts: [***.gremlin.cosmosdb.azure.com]
port: 443
username: /dbs/db/colls/coll

connectionPool: {
  enableSsl: true}
serializer: { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0, config: { serializeResultToString: true }}

So,i'm afraid that root cause is generation of token.

The input date header is invalid format. Please pass in RFC 1123 style date format.

I searched this error message related to cosmos db and i found this case for your reference.It seems that the solution is changing local regional settings.You could trace the x-ms-date header of your request with Fiddler.

Jay Gong
  • 23,163
  • 2
  • 27
  • 32
  • So I debugged the application, when requesting the permission via Azure Java SDK the `x-ms-date` headers are correctly sent. Within the Gremlin connection it is not used, I also do not know how I'd set it there. Following Azure docs, the token on a permission updates once the permission is accesses. My code does not trigger that, I request the permission (and thus the token) via REST and got an updated token. Still, the RFC issue is present also with this token. – dArignac Jan 20 '20 at 09:37
0

With Azure Support I found out that the issue was a bug on Azure side. It occurres when using custom VNets together with Cosmos. Meanwhile Azure has fixed it and everything is working.

dArignac
  • 1,205
  • 4
  • 11
  • 25