Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues using Resource Authorization Tokens #441

Open
chrisjantzen opened this issue Sep 22, 2021 · 4 comments
Open

Issues using Resource Authorization Tokens #441

chrisjantzen opened this issue Sep 22, 2021 · 4 comments
Labels
bug The issue is a bug. needs investigation The issue needs to be investigated by the maintainers or/and the community.

Comments

@chrisjantzen
Copy link
Contributor

chrisjantzen commented Sep 22, 2021

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
Copy link
Contributor Author

chrisjantzen commented Sep 23, 2021

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:

# Find the most recent token non-expired matching the resource link
$matchToken = $context.Token |
Where-Object -FilterScript { $_.Resource -eq $ResourceLink }

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.

# Try to build the authorization headers from the Context
$authorizationHeaders = Get-CosmosDbAuthorizationHeadersFromContext `
-Context $Context `
-ResourceLink $resourceLink
if ($null -eq $authorizationHeaders)
{
<#
A token in the context that matched the resource link could not
be found. So use the master key to generate the authorization headers
from the token.
#>

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.

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

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
Copy link
Owner

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
Copy link
Contributor Author

chrisjantzen commented Sep 23, 2021

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
Copy link
Contributor Author

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.

@PlagueHO PlagueHO added bug The issue is a bug. needs investigation The issue needs to be investigated by the maintainers or/and the community. labels May 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug The issue is a bug. needs investigation The issue needs to be investigated by the maintainers or/and the community.
Projects
None yet
Development

No branches or pull requests

2 participants