Azure-sdk-for-js: COSMOSDB - Unable to query with resourcetoken when we set resourcePartitionKey

Created on 4 Sep 2019  路  6Comments  路  Source: Azure/azure-sdk-for-js

"@azure/cosmos": "3.2.0"
const cosmos = require('@azure/cosmos')

const CONFIG = {
  "endpoint": "https://sqlcosmospr.documents.azure.com",
  "expiryDuration": 3600,
  "databaseId": "dsiocna1",
  "containerId": "container-dssd-kvs",
  "partitionKey": "6d7261686f"
}

async function generatePartitionToken (masterKey) {
  const client = new cosmos.CosmosClient({ endpoint: CONFIG.endpoint, key: masterKey })
  const { database } = await client.databases.createIfNotExists({ id: CONFIG.databaseId })
  const { container } = await database.containers.createIfNotExists({ id: CONFIG.containerId, partitionKey: { paths: [ '/partitionKey' ] } })
  let user
  const userId = 'user-' + CONFIG.partitionKey
  try {
    user = (await database.user(userId).read()).user
    await user.delete()
    user = (await database.users.create({ id: userId })).user
  } catch (e) {
    if (e.code !== 404) throw e
    user = (await database.users.create({ id: userId })).user
  }
  const permissionId = 'permission-' + CONFIG.partitionKey
  try {
    // delete to refresh permission
    await user.permission(permissionId).delete()
  } catch (e) {
    if (e.code !== 404) throw e
  }
  const resourceToken = (await user.permissions.create(
    {
      id: permissionId,
      permissionMode: cosmos.PermissionMode.All,
        resourcePartitionKey: CONFIG.partitionKey,
      resource: container.url
    }, {
      resourceTokenExpirySeconds: CONFIG.expiryDuration
    }
  )).resource._token
  return resourceToken
}

async function main () {
  // generate resource token restricted to partition using master key
  const resourceToken = await generatePartitionToken("Vu8uZo2aWKk3GzYU11TGcW6FnLzkpJPA8IM9UJPVk7WYOSchKLA5WEiWckuxpRd1VpCWgAYQlM1FZTLYWfPgFA==")
  console.log('resource token generated')
  // perform operations using resource token
  const container = new cosmos.CosmosClient({ endpoint: CONFIG.endpoint, tokenProvider: async () => resourceToken })
                            .database(CONFIG.databaseId)
                            .container(CONFIG.containerId)
  const key = 'mykey'
  const value = { some: 'value'}
  /// add an item using the resource token -> works fine
  const item = (await container.items.upsert({ id: key, partitionKey: CONFIG.partitionKey, value }))
  console.log('created item:', key)
  /// query all items in partition key -> BREAKS WITH 500!!
  const querryRes = await container.items.query("SELECT * from c where c.partitionKey='6d7261686f'").fetchAll()
  console.log(querryRes)

  // /// read item -> works fine
  // const data = await container.item(key, CONFIG.partitionKey).read()
  // console.log('read value:', data.resource.value)
  // /// delete item -> works fine
  // await container.item(key, CONFIG.partitionKey).delete()
  // console.log('deleted item')
}

main().catch(console.error)



md5-0189fbaae4055d697c1fe343b499f8b8



        resourcePartitionKey: CONFIG.partitionKey,



md5-5b5a5a517c43c7bab2604131a024ad5e



{ Error: Message: {"Errors":["Partition key supplied is not valid."]}
ActivityId: 1360d857-860b-45d1-8076-fc988c0d9fa0, Request URI: /apps/41c501ed-891d-41ee-a069-6e58fc9767b3/services/bc022ef3-c619-42f4-92ef-409d1ed5b22e/partitions/c99c9ba2-84de-4226-8e9c-a4d37d4cfc27/replicas/132120247861802451p, RequestStats:
RequestStartTime: 2019-09-03T23:01:22.3435778Z, RequestEndTime: 2019-09-03T23:01:22.3735777Z, Number of regions attempted: 1
ResponseTime: 2019-09-03T23:01:22.3735777Z, StoreResult: StorePhysicalAddress: rntbd://10.0.0.27:13700/apps/41c501ed-891d-41ee-a069-6e58fc9767b3/services/bc022ef3-c619-42f4-92ef-409d1ed5b22e/partitions/c99c9ba2-84de-4226-8e9c-a4d37d4cfc27/replicas/132120247861802451p, LSN: 79, GlobalCommittedLsn: 79, PartitionKeyRangeId: , IsValid: True, StatusCode: 400, SubStatusCode: 0, RequestCharge: 1.24, ItemLSN: -1, SessionToken: -1#79, UsingLocalLSN: False, TransportException: null, ResourceType: Permission, OperationType: Create
, SDK: Microsoft.Azure.Documents.Common/2.5.1
    at C:\workspace\cosmos_sql\node_modules\@azure\cosmos\dist\index.js:6389:39
    at Generator.next (<anonymous>)
    at fulfilled (C:\workspace\cosmos_sql\node_modules\tslib\tslib.js:107:62)
    at process._tickCallback (internal/process/next_tick.js:68:7)
  code: 400,
  body:
   { code: 'BadRequest',
     message:
      'Message: {"Errors":["Partition key supplied is not valid."]}\r\nActivityId: 1360d857-860b-45d1-8076-fc988c0d9fa0, Request URI: /apps/41c501ed-891d-41ee-a069-6e58fc9767b3/services/bc022ef3-c619-42f4-92ef-409d1ed5b22e/partitions/c99c9ba2-84de-4226-8e9c-a4d37d4cfc27/replicas/132120247861802451p, RequestStats: \r\nRequestStartTime: 2019-09-03T23:01:22.3435778Z, RequestEndTime: 2019-09-03T23:01:22.3735777Z, Number of regions attempted: 1\r\nResponseTime: 2019-09-03T23:01:22.3735777Z, StoreResult: StorePhysicalAddress: rntbd://10.0.0.27:13700/apps/41c501ed-891d-41ee-a069-6e58fc9767b3/services/bc022ef3-c619-42f4-92ef-409d1ed5b22e/partitions/c99c9ba2-84de-4226-8e9c-a4d37d4cfc27/replicas/132120247861802451p, LSN: 79, GlobalCommittedLsn: 79, PartitionKeyRangeId: , IsValid: True, StatusCode: 400, SubStatusCode: 0, RequestCharge: 1.24, ItemLSN: -1, SessionToken: -1#79, UsingLocalLSN: False, TransportException: null, ResourceType: Permission, OperationType: Create\r\n, SDK: Microsoft.Azure.Documents.Common/2.5.1' },
  headers:
   { 'content-type': 'application/json',
     date: 'Tue, 03 Sep 2019 23:01:21 GMT',
     lsn: '79',
     server: 'Microsoft-HTTPAPI/2.0',
     'strict-transport-security': 'max-age=31536000',
     'transfer-encoding': 'chunked',
     'x-ms-activity-id': '1360d857-860b-45d1-8076-fc988c0d9fa0',
     'x-ms-cosmos-llsn': '79',
     'x-ms-cosmos-quorum-acked-llsn': '79',
     'x-ms-current-replica-set-size': '4',
     'x-ms-current-write-quorum': '3',
     'x-ms-gatewayversion': 'version=2.5.1',
     'x-ms-global-committed-lsn': '79',
     'x-ms-last-state-change-utc': 'Tue, 03 Sep 2019 22:54:28.455 GMT',
     'x-ms-number-of-read-regions': '0',
     'x-ms-quorum-acked-lsn': '79',
     'x-ms-request-charge': '1.24',
     'x-ms-schemaversion': '1.8',
     'x-ms-serviceversion': 'version=2.5.0.0',
     'x-ms-session-token': '0:-1#79',
     'x-ms-transport-request-id': '607392',
     'x-ms-xp-role': '1',
     'x-ms-throttle-retry-count': 0,
     'x-ms-throttle-retry-wait-time-ms': 0 },
  activityId: '1360d857-860b-45d1-8076-fc988c0d9fa0' }



md5-3723f14868083f039b1c469efb6adefd



        resourcePartitionKey: [CONFIG.partitionKey],



md5-62d668c4003f9d9ec58c7ef6c0b6fb9d



C:\workspace\cosmos_sql>node test.js
resource token generated
created item: mykey
{ Error: Insufficient permissions provided in the authorization header for the corresponding request. Please retry with another authorization header.
ActivityId: fe4b8eef-49c1-4c89-b19c-69517b9b1790, Microsoft.Azure.Documents.Common/2.5.1
    at C:\workspace\cosmos_sql\node_modules\@azure\cosmos\dist\index.js:6389:39
    at Generator.next (<anonymous>)
    at fulfilled (C:\workspace\cosmos_sql\node_modules\tslib\tslib.js:107:62)
    at process._tickCallback (internal/process/next_tick.js:68:7)
  code: 403,
  body:
   { code: 'Forbidden',
     message:
      'Insufficient permissions provided in the authorization header for the corresponding request. Please retry with another authorization header.\r\nActivityId: fe4b8eef-49c1-4c89-b19c-69517b9b1790, Microsoft.Azure.Documents.Common/2.5.1' },
  headers:
   { 'content-type': 'application/json',
     date: 'Tue, 03 Sep 2019 22:59:17 GMT',
     server: 'Microsoft-HTTPAPI/2.0',
     'strict-transport-security': 'max-age=31536000',
     'transfer-encoding': 'chunked',
     'x-ms-activity-id': 'fe4b8eef-49c1-4c89-b19c-69517b9b1790',
     'x-ms-gatewayversion': 'version=2.5.1',
     'x-ms-throttle-retry-count': 0,
     'x-ms-throttle-retry-wait-time-ms': 0 },
  activityId: 'fe4b8eef-49c1-4c89-b19c-69517b9b1790' }

could you help us use PermissionDefination with resourcePartitionKey
https://docs.microsoft.com/en-us/javascript/api/@azure/cosmos/permissiondefinition?view=azure-node-latest#resourcepartitionkey

Client Cosmos bug

All 6 comments

So the correct way is:

const resourceToken = (await user.permissions.create(
    {
      id: permissionId,
      permissionMode: cosmos.PermissionMode.All,
      resourcePartitionKey: [CONFIG.partitionKey],
      resource: container.url
    },
    {
      resourceTokenExpirySeconds: CONFIG.expiryDuration
    }
  )).resource._token;

but query won't work unless you specify the partition key via a header (which we removed in V3). You can work around it by overriding the header:

const querryRes = await container.items
    .query("SELECT * from c where c.partitionKey ='6d7261686f'", {
      initialHeaders: {
        "x-ms-documentdb-partitionkey": '["6d7261686f"]'
      }
    })
    .fetchAll();


Here's a full sample

//@ts-check
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;

const cosmos = require("@azure/cosmos");

const CONFIG = {
  endpoint: "https://localhost:8081",
  expiryDuration: 3600,
  databaseId: "dsiocna1",
  containerId: "container-dssd-kvs",
  partitionKey: "6d7261686f"
};

async function generatePartitionToken(masterKey) {
  const client = new cosmos.CosmosClient({
    endpoint: CONFIG.endpoint,
    key: masterKey
  });
  const { database } = await client.databases.createIfNotExists({
    id: CONFIG.databaseId
  });
  const { container } = await database.containers.createIfNotExists({
    id: CONFIG.containerId,
    partitionKey: { paths: ["/partitionKey"] }
  });
  let user;
  const userId = "user-" + CONFIG.partitionKey;
  try {
    user = (await database.user(userId).read()).user;
    await user.delete();
    user = (await database.users.create({ id: userId })).user;
  } catch (e) {
    if (e.code !== 404) throw e;
    user = (await database.users.create({ id: userId })).user;
  }
  const permissionId = "permission-" + CONFIG.partitionKey;
  try {
    // delete to refresh permission
    await user.permission(permissionId).delete();
  } catch (e) {
    if (e.code !== 404) throw e;
  }
  const resourceToken = (await user.permissions.create(
    {
      id: permissionId,
      permissionMode: cosmos.PermissionMode.All,
      resourcePartitionKey: [CONFIG.partitionKey],
      resource: container.url
    },
    {
      resourceTokenExpirySeconds: CONFIG.expiryDuration
    }
  )).resource._token;
  return resourceToken;
}

async function main() {
  // generate resource token restricted to partition using master key
  const resourceToken = await generatePartitionToken(
    "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
  );
  console.log("resource token generated");
  // perform operations using resource token
  const container = new cosmos.CosmosClient({
    endpoint: CONFIG.endpoint,
    tokenProvider: async () => resourceToken
  })
    .database(CONFIG.databaseId)
    .container(CONFIG.containerId);
  const key = "mykey";
  const value = { some: "value" };
  /// add an item using the resource token -> works fine
  const item = await container.items.upsert({
    id: key,
    partitionKey: CONFIG.partitionKey,
    value
  });
  console.log("created item:", key);
  /// query all items in partition key -> BREAKS WITH 500!!
  const querryRes = await container.items
    .query("SELECT * from c where c.partitionKey ='6d7261686f'", {
      initialHeaders: {
        "x-ms-documentdb-partitionkey": '["6d7261686f"]'
      }
    })
    .fetchAll();
  console.log(querryRes);

  // /// read item -> works fine
  // const data = await container.item(key, CONFIG.partitionKey).read()
  // console.log('read value:', data.resource.value)
  // /// delete item -> works fine
  // await container.item(key, CONFIG.partitionKey).delete()
  // console.log('deleted item')
}

main().catch(err => {
  console.error(err);
});

@southpolesteve - this is tricky. We should probably expose partitionKey on FeedOptions since it's easy. I'll reach out to the query team to see why they aren't parsing the partitionKey out, but there's a good chance they say this is by design.

Thanks @christopheranderson . I was able to make it work with workaround mentioned above.

This workaround works but quite ugly.

The typical use case for resource tokens is for 3rd parties to access to their restricted, aka partitioned, section of the whole container.items.

But as for me, the platform, aka API endpoints, we do not want to let the 3rd parties to know their being restricted.

So when the 3rd parties run query like SELECT VALUE COUNT(1) FROM c, it would return the full count, like 100, if no partition key specificed in resource tokens, or a partial count of their partitioned items, like 6, with partitionkey specified in resource tokens.

partition key should work like an embedded/hidden filter. But this workaround would make it way too ugly to hide.

Somehow, the 3rd party should not be aware of their partitionkey at all.

P.S. the doc of initialheaders looks quite scary.

https://docs.microsoft.com/en-us/javascript/api/@azure/cosmos/feedoptions?view=azure-node-latest#initialheaders

@gwh-cpnet We added partitionKey back to the feed options in 3.6.3 https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/cosmosdb/cosmos/CHANGELOG.md#363-2020-4-08 so initial headers should not be needed anymore.

Unfortunately completely hiding the partition key is difficult. The resource tokens for Cosmos do not contain metadata for operations. IE, the SDK has no way to tell from a single resource token which partition key it belongs to. The caller must provide this information.

@southpolesteve Thanks for feedback.

I have verified that replacing the initialHeaders with partitionKey property works fine.

Even though resource token is lack of metadata, calling cosmos.user(userId).permissions.readAll().fetchAll() would return newly generated token along with resource and resourcePartitionKey (in array form). So it is okay for me to forward its result to the 3rd parties I was talking about.

Another strange thing, or bug in my opinion, that when calling cosmos.user(userId).permissions.create(), the partitionKey value has to be an array, while raw string would raise an error. but the doc says they are both acceptable.

However in the case of reading items, both string and array of string work fine.

Was this page helpful?
0 / 5 - 0 ratings