PlagueHO / CosmosDB

PowerShell Module for working with Azure Cosmos DB databases, collections, documents, attachments, offers, users, permissions, triggers, stored procedures and user defined functions.
http://dscottraynsford.com
MIT License
152 stars 46 forks source link

Issues using Resource Authorization Tokens #441

Open chrisjantzen opened 2 years ago

chrisjantzen commented 2 years ago

I am trying to setup a key broker and am struggling to get it working. I used the example to setup a User and configured a Permission on that user, this seemed to work smoothly, I then create a Resource Auth Token but when using the Resource Context I am getting an error. Here is the code I have used so far:

# Create the user and permissions
New-CosmosDbUser -Context $cosmosDbContext -Id 'Test'
New-CosmosDbPermission -Context $cosmosDbContext -UserId 'Test' -Id 'all_users' -Resource "dbs/Usage/colls/Users" -PermissionMode All

$TokenLife = 3600

$permission = Get-CosmosDbPermission -Context $cosmosDbContext -UserId 'Test' -Id 'all_users' -TokenExpiry $TokenLife

$contextToken = New-CosmosDbContextToken `
            -Resource $permission[0].Resource `
            -TimeStamp (Get-Date) `
            -TokenExpiry $TokenLife `
            -Token (ConvertTo-SecureString -String $permission[0].Token -AsPlainText -Force)

$resourceContext = New-CosmosDbContext -Account $CosmosDBAccount -Database 'Usage' -Token $contextToken

The TimeStamp is the main thing done differently from the example but I found the example didn't work quite right and this seemed to work well enough as a workaround. I also hardcoded the resource path, but I have also tested Get-CosmosDbCollectionResourcePath and end up with the same result.

This all appears to work and creates a context object with the Token set:

Account       : stats
Database      : Usage
Key           :
KeyType       : master
BaseUri       : https://stats.documents.azure.com/
Token         : {CosmosDB.ContextToken}
BackoffPolicy :
Environment   : AzureCloud

If I try to use this context object to get the Users collection's info like in the example it works fine, but if I try to query documents I get an error.

This works: Get-CosmosDbCollection -Context $resourceContext -Id 'Users'

This does not:

$Query = "SELECT * FROM Users u"
$ExistingUsers = Get-CosmosDbDocument -Context $resourceContext -Database 'Usage' -CollectionId "Users" -Query $Query -PartitionKey 'user'

It returns the following error: VERBOSE: Searching context tokens for resource matching 'dbs/Usage/colls/Users/docs'. VERBOSE: Context token with resource 'dbs/Usage/colls/Users/docs' not found. System.InvalidOperationException: The authorization key is empty. It must be passed in the context or a valid token context for the resource being accessed must be supplied.

If anyone has any suggestions or could point me in the right direction, I'd really appreciate it! I've went through the code but can't figure out why it's not working. It seems like the permission's aren't correct for this. I did try setting a permission on the resource dbs/Usage/colls/Users/docs, but it wouldn't take that as a valid option.

chrisjantzen commented 2 years ago

I did a deep-dive into the module's code today and I think I've found the issue.

In the Get-CosmosDbAuthorizationHeadersFromContext util the code checks for a valid token where the token's resource matches the resource you are querying: https://github.com/PlagueHO/CosmosDB/blob/f6d628cdc3066fafc94c8624a4893fbe2c1cfa55/source/Private/utils/Get-CosmosDbAuthorizationHeadersFromContext.ps1#L23-L25

When creating a token, the resource connected to the token looks like dbs/Usage/colls/Users, and as noted above, you cannot set the resource to dbs/Usage/colls/Users/docs. If you simply query a collection (like in the example for resource tokens) the resource looks like dbs/Usage/colls/Users. This matches so $matchToken is set. If you query a document in that collection, the resource becomes dbs/Usage/colls/Users/docs and no match is found. Therefore no auth headers can be made from a token and in the Invoke-CosmosDbRequest function it falls back to looking for a master key.

https://github.com/PlagueHO/CosmosDB/blob/f6d628cdc3066fafc94c8624a4893fbe2c1cfa55/source/Private/utils/Invoke-CosmosDbRequest.ps1#L144-L155

If no master key is set, the query fails. I believe it should allow sub resources in the token check and that this is an issue in the module code.

Looking at Invoke-CosmosDbRequest, we actually seem to have all the data we need to make this work. The $resourceId is a parsed version of the resource link which in this case strips off docs making the resource id dbs/Usage/colls/Users, which will work in the resource check in this scenario. I'm not certain, but I believe it should work in most other scenario's as well.

https://github.com/PlagueHO/CosmosDB/blob/f6d628cdc3066fafc94c8624a4893fbe2c1cfa55/source/Private/utils/Invoke-CosmosDbRequest.ps1#L128-L136

I attempted a fix: Lines 80 to 147 of source/Private/utils/Invoke-CosmosDbRequest.ps1

    # Generate the resource link value that will be used in the URI and to generate the resource id
    switch ($resourceType)
    {
        'dbs'
        {
            # Request for a database object (not containined in a database)
            if ([System.String]::IsNullOrEmpty($ResourcePath))
            {
                $ResourceLink = 'dbs'
                $AuthLink = 'dbs' # +++
            }
            else
            {
                $resourceLink = $ResourcePath
                $resourceId = $resourceLink
                $authLink = $resourceId # +++
            }
        }

        'offers'
        {
            # Request for an offer object (not contained in a database)
            if ([System.String]::IsNullOrEmpty($ResourcePath))
            {
                $ResourceLink = 'offers'
                $AuthLink = 'offers' # +++
            }
            else
            {
                $resourceLink = $ResourcePath
                $resourceId = ($ResourceLink -split '/')[1].ToLowerInvariant()
                $authLink = $resourceId # +++
            }
        }

        default
        {
            # Request for an object that is within a database
            $resourceLink = ('dbs/{0}' -f $Database)

            if ($PSBoundParameters.ContainsKey('ResourcePath'))
            {
                $resourceLink = ('{0}/{1}' -f $resourceLink, $ResourcePath)
            }
            else
            {
                $resourceLink = ('{0}/{1}' -f $resourceLink, $ResourceType)
            }

            # Generate the resource Id from the resource link value
            $resourceElements = [System.Collections.ArrayList] ($resourceLink -split '/')

            if (($resourceElements.Count % 2) -eq 0)
            {
                $resourceId = $resourceLink
            }
            else
            {
                $resourceElements.RemoveAt($resourceElements.Count - 1)
                $resourceId = $resourceElements -Join '/'
            }
            $authLink = $resourceId # +++
        }
    }

    # Generate the URI from the base connection URI and the resource link
    $baseUri = $Context.BaseUri.ToString()
    $uri = [uri]::New(('{0}{1}' -f $baseUri, $resourceLink))

    # Try to build the authorization headers from the Context
    $authorizationHeaders = Get-CosmosDbAuthorizationHeadersFromContext `
        -Context $Context `
        -ResourceLink $authLink # changed from $resourceLink to $authLink

In the above code I've simply added the $authLink, then used that for the auth headers function. We could use $resourceId but it isn't set for the dbs or offers cases and I didn't want to risk setting it and breaking something.

TLDR / Summary I've tested the above code out and it seems to work in this scenario. I can query documents in this collection as well as the collection directly. Considering this works, it seems to be this is the way the Cosmos DB api permissions are meant to work, the issue is this module doing a resource link check for validation, and preventing the query. I'm not certain my above fix is the best option, which is why I haven't put in a commit. Though it seems that this check needs to be more generalized to allow the base resource link (up to the collection) to work for sub resource links (such as docs).

Considering the resource tokens are not a new feature, and this seems like a pretty big issue that prevents them from being all that useable, I could be way off target. I suspect this isn't a commonly used part of the module but I'm still surprised the issue hasn't come up before. Have I missed something?

PlagueHO commented 2 years ago

Hi @chrisjantzen - thank you for raising this and all the investigation. You're right - this doesn't get a lot of use, but I think it is covered by the automated integration tests. Let me spend some time this weekend digging into the desired function of this feature and see if I've made some mistakes.

What I'm scratching my head over is why the resource token context link doesn't match the query context link? E.g. Usage vs DeviceUsage? Do you know why that is happening? Seems like they're reporting as different databases.

chrisjantzen commented 2 years ago

What I'm scratching my head over is why the resource token context link doesn't match the query context link? E.g. Usage vs DeviceUsage? Do you know why that is happening? Seems like they're reporting as different databases.

Thanks you very much @PlagueHO! That part's my bad. I created the test database 'Usage' and my testing was split between a live 'DeviceUsage' and test 'Usage' DB. They were setup the same way so the data should all match. I've updated the post so that it all references the same database now.

chrisjantzen commented 2 years ago

I've been using a modified module based on the above code and found an issue with it. This does not work when updating documents as the path contains 6 parts (and therefore it's even so the resource ID doesn't get stripped down). It seems like the authLink should always be the first 4 parts of the resource link as that should always equated to the collection itself.

This is a modification to the 'default' section of the switch in Invoke-CosmosDBRequest in the above code. Lines 111 to 137 of source/Private/utils/Invoke-CosmosDbRequest.ps1

default
        {
            # Request for an object that is within a database
            $resourceLink = ('dbs/{0}' -f $Database)

            if ($PSBoundParameters.ContainsKey('ResourcePath'))
            {
                $resourceLink = ('{0}/{1}' -f $resourceLink, $ResourcePath)
            }
            else
            {
                $resourceLink = ('{0}/{1}' -f $resourceLink, $ResourceType)
            }

            # Generate the resource Id from the resource link value
            $resourceElements = [System.Collections.ArrayList] ($resourceLink -split '/')
            if (($resourceElements.Count % 2) -eq 0)
            {
                $resourceId = $resourceLink
            }
            else
            {
                $resourceElements.RemoveAt($resourceElements.Count - 1)
                $resourceId = $resourceElements -Join '/'
            }

            # Generate the collection's path for token auth purposes
            if ($resourceElements.Count -le 4)
            {
                $authLink = $resourceId
            }
            else
            {
                $authLink = $resourceElements[0..3] -Join '/'
            }
        }

This just uses the resourceLink unless it has greater than 4 parts, and in that case it takes the first 4 parts of the path. I've tested this with gets, sets, and inserts so far.