diff --git a/CHANGELOG.md b/CHANGELOG.md index 8305576..554fc2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Release Notes All notable changes and release history of the "cosmos-db" module will be documented in this file. +## 1.19 +* Changed to Entra Id authentication by default. +* Adds a `-enableMasterKeyAuth` flag to `Use-CosmosDbInternalFlag` which reverts auth to use master keys. + ## 1.18 * Fixes a bug in commands like `Search-CosmosDbRecords` and `Get-AllCosmosDbRecords` which might run for long enough that their auth tokens expire and aren't refreshed. Auth tokens will now be refreshed every 10 min as these commands run. * Adds a `-enableAuthHeaderReuse` flag to `Use-CosmosDbInternalFlag` which disables the 10 minute refresh period and forces auth header refreshes for every API call. diff --git a/README.md b/README.md index 4310f67..2d7b066 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ Use-CosmosDbInternalFlag -EnableFiddlerDebugging $true | EnableCaching | Enables/disables caching certain values like DB keys, partition ranges, etc. Improves performance of nearly all operations. | No - default is enabled | | EnablePartitionKeyRangeSearches | **[Experimental]** Enables/disables filtering `Search` queries to relevant partition ranges instead of a full scan. Improves performance of `Search` commands. | No - default is disabled | | EnableAuthHeaderReuse | Enables/disables reusing auth headers for commands which use continuation tokens, like `Search-CosmosDbRecords` or `Get-AllCosmosDbRecords`. | No - default is enabled | +| EnableMasterKeyAuth | Enables/disables using master key based authentication instead of Entra Id. | No - default is disabled | ## Error Handling diff --git a/cosmos-db/cosmos-db.psd1 b/cosmos-db/cosmos-db.psd1 index 0099a80..3d5bd90 100644 --- a/cosmos-db/cosmos-db.psd1 +++ b/cosmos-db/cosmos-db.psd1 @@ -11,7 +11,7 @@ # RootModule = '' # Version number of this module. - ModuleVersion = '1.18' + ModuleVersion = '1.19' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/cosmos-db/cosmos-db.psm1 b/cosmos-db/cosmos-db.psm1 index 842bc55..b5f0521 100644 --- a/cosmos-db/cosmos-db.psm1 +++ b/cosmos-db/cosmos-db.psm1 @@ -15,6 +15,7 @@ $API_VERSION = "2018-12-31" $AUTHORIZATION_HEADER_REFRESH_THRESHOLD = [System.TimeSpan]::FromMinutes(10); $MASTER_KEY_CACHE = @{} +$AAD_TOKEN_CACHE = @{} $SIGNATURE_HASH_CACHE = @{} $PARTITION_KEY_RANGE_CACHE = @{} @@ -57,13 +58,23 @@ Function Get-CacheValue([string]$key, [hashtable]$cache) { return $null } -Function Set-CacheValue([string]$key, $value, [hashtable]$cache, [int]$expirationHours) { +Function Set-CacheValue([string]$key, $value, [hashtable]$cache, [int]$expirationHours, [int]$expirationMinutes) { $cache[$key] = @{ - Expiration = [datetime]::UtcNow.AddHours($expirationHours); + Expiration = [datetime]::UtcNow.AddHours($expirationHours).AddMinutes($expirationMinutes); Value = $value } } +Function RequireWriteableKey() { + # Entra Id auth does not support read only tokens (because it's just handled by RBAC). + # So if the user requested to be in read only mode, we have to manage it ourselves. + + $readonlyEnabled = $env:COSMOS_DB_FLAG_ENABLE_READONLY_KEYS -eq 1 + if ($readonlyEnabled) { + throw "Operation not allowed in readonly mode" + } +} + Function Get-Base64Masterkey([string]$ResourceGroup, [string]$Database, [string]$SubscriptionId) { $readonly = $env:COSMOS_DB_FLAG_ENABLE_READONLY_KEYS -eq 1 @@ -99,6 +110,31 @@ Function Get-Base64MasterkeyWithoutCaching([string]$ResourceGroup, [string]$Data $masterKey } +Function Get-AADToken([string]$ResourceGroup, [string]$Database, [string]$SubscriptionId) { + $cacheKey = "aadtoken" + $cacheResult = Get-CacheValue -Key $cacheKey -Cache $AAD_TOKEN_CACHE + if ($cacheResult) { + return $cacheResult + } + + $oauth = Get-AADTokenWithoutCaching + $token = $oauth.AccessToken + + $expirationUtcTimestamp = $oauth.expires_on + $expiration = [System.DateTimeOffset]::FromUnixTimeSeconds($expirationUtcTimestamp).DateTime + $remainingTime = $expiration - [System.DateTime]::UtcNow + $cacheExpirationMinutes = ($remainingTime - $AUTHORIZATION_HEADER_REFRESH_THRESHOLD).Minutes + + Set-CacheValue -Key $cacheKey -Value $token -Cache $AAD_TOKEN_CACHE -ExpirationMinutes $cacheExpirationMinutes + + $token +} + +# This is just to support testing caching with Get-AADToken and isn't meant to be used directly +Function Get-AADTokenWithoutCaching() { + az account get-access-token --resource "https://cosmos.azure.com" | ConvertFrom-Json +} + Function Get-Signature([string]$verb, [string]$resourceType, [string]$resourceUrl, [string]$now) { $parts = @( $verb.ToLower(), @@ -128,19 +164,25 @@ Function Get-Base64EncryptedSignatureHash([string]$masterKey, [string]$signature $base64Hash } -Function Get-EncodedAuthString([string]$signatureHash) { - $authString = "type=master&ver=1.0&sig=$signatureHash" +Function Get-EncodedAuthString([string]$signatureHash, [string]$type) { + $authString = "type=$type&ver=1.0&sig=$signatureHash" [uri]::EscapeDataString($authString) } Function Get-AuthorizationHeader([string]$ResourceGroup, [string]$SubscriptionId, [string]$Database, [string]$verb, [string]$resourceType, [string]$resourceUrl, [string]$now) { - $masterKey = Get-Base64Masterkey -ResourceGroup $ResourceGroup -Database $Database -SubscriptionId $SubscriptionId + if ($env:COSMOS_DB_FLAG_ENABLE_MASTER_KEY_AUTH -eq 1) { + $masterKey = Get-Base64Masterkey -ResourceGroup $ResourceGroup -Database $Database -SubscriptionId $SubscriptionId - $signature = Get-Signature -verb $verb -resourceType $resourceType -resourceUrl $resourceUrl -now $now + $signature = Get-Signature -verb $verb -resourceType $resourceType -resourceUrl $resourceUrl -now $now + + $signatureHash = Get-Base64EncryptedSignatureHash -masterKey $masterKey -signature $signature + + return Get-EncodedAuthString -signatureHash $signatureHash -type "master" + } - $signatureHash = Get-Base64EncryptedSignatureHash -masterKey $masterKey -signature $signature + $token = Get-AADToken - Get-EncodedAuthString -signatureHash $signatureHash + return Get-EncodedAuthString -signatureHash $token -type "aad" } Function Get-CommonHeaders([string]$now, [string]$encodedAuthString, [string]$contentType = "application/json", [bool]$isQuery = $false, [string]$PartitionKey = $null, [string]$Etag = $null) { @@ -268,7 +310,7 @@ Function Invoke-CosmosDbApiRequestWithContinuation([string]$verb, [string]$url, $headers["x-ms-continuation"] = $continuationToken $authHeaderReuseDisabled = $env:COSMOS_DB_FLAG_ENABLE_AUTH_HEADER_REUSE -eq 0 - $authHeaderExpired = [System.DateTime]::Parse($authHeaders.now) + $AUTHORIZATION_HEADER_REFRESH_THRESHOLD -lt [System.DateTime]::UtcNow + $authHeaderExpired = [System.DateTimeOffset]::Parse($authHeaders.now) + $AUTHORIZATION_HEADER_REFRESH_THRESHOLD -lt [System.DateTime]::UtcNow if ($authHeaderReuseDisabled -or $authHeaderExpired) { $authHeaders = Invoke-Command -ScriptBlock $refreshAuthHeaders @@ -746,6 +788,8 @@ Function New-CosmosDbRecord { ) begin { + RequireWriteableKey + $baseUrl = Get-BaseDatabaseUrl $Database $collectionsUrl = Get-CollectionsUrl $Container $Collection $docsUrl = "$collectionsUrl/$DOCS_TYPE" @@ -833,6 +877,8 @@ Function Update-CosmosDbRecord { ) begin { + RequireWriteableKey + $baseUrl = Get-BaseDatabaseUrl $Database } process { @@ -929,6 +975,8 @@ Function Remove-CosmosDbRecord { ) begin { + RequireWriteableKey + $baseUrl = Get-BaseDatabaseUrl $Database } process { @@ -1005,7 +1053,8 @@ Function Use-CosmosDbInternalFlag $enableFiddlerDebugging = $null, $enableCaching = $null, $enablePartitionKeyRangeSearches = $null, - $enableAuthHeaderReuse = $null + $enableAuthHeaderReuse = $null, + $enableMasterKeyAuth = $null ) { if ($null -ne $enableFiddlerDebugging) { $env:AZURE_CLI_DISABLE_CONNECTION_VERIFICATION = if ($enableFiddlerDebugging) { 1 } else { 0 } @@ -1022,6 +1071,10 @@ Function Use-CosmosDbInternalFlag if ($null -ne $enableAuthHeaderReuse) { $env:COSMOS_DB_FLAG_ENABLE_AUTH_HEADER_REUSE = if ($enableAuthHeaderReuse) { 1 } else { 0 } } + + if ($null -ne $enableMasterKeyAuth) { + $env:COSMOS_DB_FLAG_ENABLE_MASTER_KEY_AUTH = if ($enableMasterKeyAuth) { 1 } else { 0 } + } } Function Use-CosmosDbReadonlyKeys diff --git a/tests/Get-AadToken.Tests.ps1 b/tests/Get-AadToken.Tests.ps1 new file mode 100644 index 0000000..3b086ca --- /dev/null +++ b/tests/Get-AadToken.Tests.ps1 @@ -0,0 +1,67 @@ +Get-Module cosmos-db | Remove-Module -Force +Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force + +InModuleScope cosmos-db { + Describe "Get-AadToken" { + BeforeAll { + $MOCK_TOKEN = @{ + accessToken = "MOCK_TOKEN" + expires_on = [System.DateTimeOffset]::UtcNow.AddHours(1).ToUnixTimeSeconds() + } + + Mock Get-AadTokenWithoutCaching { + return $MOCK_TOKEN + } + } + + BeforeEach { + # This is defined in the main module + $AAD_TOKEN_CACHE = @{} + } + + It "Only calls the core logic once with caching enabled" { + Use-CosmosDbInternalFlag -EnableCaching $true + + $key = Get-AadToken + $key | Should -Be $MOCK_TOKEN.accessToken | Out-Null + + $key = Get-AadToken + $key | Should -Be $MOCK_TOKEN.accessToken | Out-Null + + Assert-MockCalled Get-AadTokenWithoutCaching -Times 1 -Exactly + } + + It "Calls the core logic for each call with caching disabled" { + Use-CosmosDbInternalFlag -EnableCaching $false + + $key1 = Get-AadToken + $key1 | Should -Be $MOCK_TOKEN.accessToken | Out-Null + + $key2 = Get-AadToken + $key2 | Should -Be $MOCK_TOKEN.accessToken | Out-Null + + Assert-MockCalled Get-AadTokenWithoutCaching -Times 2 -Exactly + } + + It "Respects token expiration" { + Use-CosmosDbInternalFlag -EnableCaching $true + + $MOCK_TOKEN = @{ + accessToken = "MOCK_TOKEN" + expires_on = [System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + } + + Mock Get-AadTokenWithoutCaching { + return $MOCK_TOKEN + } + + $key1 = Get-AadToken + $key1 | Should -Be $MOCK_TOKEN.accessToken | Out-Null + + $key2 = Get-AadToken + $key2 | Should -Be $MOCK_TOKEN.accessToken | Out-Null + + Assert-MockCalled Get-AadTokenWithoutCaching -Times 2 -Exactly + } + } +} \ No newline at end of file diff --git a/tests/Get-AuthorizationHeader.Tests.ps1 b/tests/Get-AuthorizationHeader.Tests.ps1 index be6a7e4..92c5c9b 100644 --- a/tests/Get-AuthorizationHeader.Tests.ps1 +++ b/tests/Get-AuthorizationHeader.Tests.ps1 @@ -13,6 +13,7 @@ InModuleScope cosmos-db { $MOCK_RESOURCE_TYPE = "MOCK_RESOURCE_TYPE" $MOCK_VERB = "MOCK_VERB" $MOCK_NOW = "MOCK_NOW" + $MOCK_AAD_TOKEN = "MOCK_AAD_TOKEN" $MOCK_MASTER_KEY_BYTES = [System.Text.Encoding]::UTF8.GetBytes('gVkYp3s6v9y$B&E)H@MbQeThWmZq4t7w') @@ -25,17 +26,27 @@ InModuleScope cosmos-db { [System.Convert]::ToBase64String($MOCK_MASTER_KEY_BYTES) } + + Mock Get-AADToken { + return $MOCK_AAD_TOKEN + } + } + + AfterEach { + $env:COSMOS_DB_FLAG_ENABLE_MASTER_KEY_AUTH = $null } - It "Returns the correct signature hashed with the master key" { - $result = Get-AuthorizationHeader -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Verb $MOCK_VERB -ResourceType $MOCK_RESOURCE_TYPE -ResourceUrl $MOCK_RESOURCE_URL -Now $MOCK_NOW + It "Returns the correct signature hashed with the master key" { + Use-CosmosDbInternalFlag -enableMasterKeyAuth $true + + $result = Get-AuthorizationHeader -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Verb $MOCK_VERB -ResourceType $MOCK_RESOURCE_TYPE -ResourceUrl $MOCK_RESOURCE_URL -Now $MOCK_NOW $expectedSignature = "$($MOCK_VERB.ToLower())`n$($MOCK_RESOURCE_TYPE.ToLower())`n$MOCK_RESOURCE_URL`n$($MOCK_NOW.ToLower())`n`n" $hasher = New-Object System.Security.Cryptography.HMACSHA256 -Property @{ Key = $MOCK_MASTER_KEY_BYTES } - $sigBinary=[System.Text.Encoding]::UTF8.GetBytes($expectedSignature) - $hashBytes=$hasher.ComputeHash($sigBinary) - $expectedBase64Hash=[System.Convert]::ToBase64String($hashBytes) + $sigBinary = [System.Text.Encoding]::UTF8.GetBytes($expectedSignature) + $hashBytes = $hasher.ComputeHash($sigBinary) + $expectedBase64Hash = [System.Convert]::ToBase64String($hashBytes) $expectedHeader = [uri]::EscapeDataString("type=master&ver=1.0&sig=$expectedBase64Hash") @@ -43,5 +54,15 @@ InModuleScope cosmos-db { Assert-MockCalled Get-Base64Masterkey -Times 1 } + + It "Returns the correct signature with for entra id auth" { + $result = Get-AuthorizationHeader -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Verb $MOCK_VERB -ResourceType $MOCK_RESOURCE_TYPE -ResourceUrl $MOCK_RESOURCE_URL -Now $MOCK_NOW + + $expectedHeader = [uri]::EscapeDataString("type=aad&ver=1.0&sig=$MOCK_AAD_TOKEN") + + $result | Should -Be $expectedHeader + + Assert-MockCalled Get-AADToken -Times 1 + } } } \ No newline at end of file diff --git a/tests/Get-Base64Masterkey.Tests.ps1 b/tests/Get-Base64Masterkey.Tests.ps1 index d039010..7fd3095 100644 --- a/tests/Get-Base64Masterkey.Tests.ps1 +++ b/tests/Get-Base64Masterkey.Tests.ps1 @@ -26,6 +26,10 @@ InModuleScope cosmos-db { } } + AfterAll { + $env:COSMOS_DB_FLAG_ENABLE_READONLY_KEYS = $null + } + BeforeEach { # This is defined in the main module $MASTER_KEY_CACHE = @{} diff --git a/tests/Get-CosmosDbRecordContent.Tests.ps1 b/tests/Get-CosmosDbRecordContent.Tests.ps1 index 815a16a..bea32fb 100644 --- a/tests/Get-CosmosDbRecordContent.Tests.ps1 +++ b/tests/Get-CosmosDbRecordContent.Tests.ps1 @@ -9,6 +9,10 @@ InModuleScope cosmos-db { . $PSScriptRoot\Utils.ps1 } + AfterAll { + $env:COSMOS_DB_FLAG_ENABLE_READONLY_KEYS = $null + } + It "Returns the Content of a successful response" { $content = @{ Key1 = "Value1"; diff --git a/tests/Invoke-CosmosDbApiRequestWithContinuation.Tests.ps1 b/tests/Invoke-CosmosDbApiRequestWithContinuation.Tests.ps1 index 7f549e6..01de382 100644 --- a/tests/Invoke-CosmosDbApiRequestWithContinuation.Tests.ps1 +++ b/tests/Invoke-CosmosDbApiRequestWithContinuation.Tests.ps1 @@ -387,7 +387,6 @@ InModuleScope cosmos-db { } It "Does not refresh auth headers before age threshold" { - # Force each call to refresh $AUTHORIZATION_HEADER_REFRESH_THRESHOLD = [System.TimeSpan]::FromHours(1) $continuationTokens = @($null, "100", "200", "300") diff --git a/tests/New-CosmosDbRecord.Tests.ps1 b/tests/New-CosmosDbRecord.Tests.ps1 index 520e49d..472945b 100644 --- a/tests/New-CosmosDbRecord.Tests.ps1 +++ b/tests/New-CosmosDbRecord.Tests.ps1 @@ -3,8 +3,9 @@ Import-Module $PSScriptRoot\..\cosmos-db\cosmos-db.psm1 -Force InModuleScope cosmos-db { Describe "New-CosmosDbRecord" { - BeforeAll { + BeforeEach { Use-CosmosDbInternalFlag -EnableCaching $false + Use-CosmosDbReadonlyKeys -Disable . $PSScriptRoot\Utils.ps1 @@ -19,8 +20,7 @@ InModuleScope cosmos-db { $MOCK_AUTH_HEADER = "MockAuthHeader" - Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) - { + Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now) { $ResourceGroup | Should -Be $MOCK_RG $SubscriptionId | Should -Be $MOCK_SUB @@ -29,8 +29,7 @@ InModuleScope cosmos-db { $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION" } - Function VerifyInvokeCosmosDbApiRequest($verb, $url, $actualBody, $expectedBody, $headers, $partitionKey=$MOCK_RECORD_ID) - { + Function VerifyInvokeCosmosDbApiRequest($verb, $url, $actualBody, $expectedBody, $headers, $partitionKey = $MOCK_RECORD_ID) { $verb | Should -Be "post" $url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs" @@ -54,14 +53,26 @@ InModuleScope cosmos-db { } } + It "Should throw in read only mode" { + Use-CosmosDbReadonlyKeys + + $payload = @{ + id = $MOCK_RECORD_ID; + key1 = "value1"; + key2 = 2; + } + + { $payload | New-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION } | Should -Throw "Operation not allowed in readonly mode" + } + It "Sends correct request with default partition key" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $payload = @{ - id = $MOCK_RECORD_ID; + id = $MOCK_RECORD_ID; key1 = "value1"; key2 = 2; } @@ -84,11 +95,11 @@ InModuleScope cosmos-db { It "Sends correct request with custom explicit partition key" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $payload = @{ - id = $MOCK_RECORD_ID; + id = $MOCK_RECORD_ID; key1 = "value1"; key2 = 2; } @@ -113,11 +124,11 @@ InModuleScope cosmos-db { It "Sends correct request with custom partition key callback" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $payload = @{ - id = $MOCK_RECORD_ID; + id = $MOCK_RECORD_ID; key1 = "value1"; key2 = 2; } @@ -148,9 +159,9 @@ InModuleScope cosmos-db { It "Sends correct request with custom partition key callback for multiple inputs" { $payloads = @( - @{ id = "1" }; - @{ id = "2" }; - @{ id = "3" }; + @{ id = "1" }; + @{ id = "2" }; + @{ id = "3" }; ) $global:idx = 0 @@ -174,7 +185,7 @@ InModuleScope cosmos-db { $response = @{ StatusCode = 200; - Content = $global:idx; + Content = $global:idx; } $global:expectedResponses += $response @@ -196,7 +207,7 @@ InModuleScope cosmos-db { $recordResponse = [PSCustomObject]@{} $payload = @{ - id = $MOCK_RECORD_ID; + id = $MOCK_RECORD_ID; key1 = "value1"; key2 = 2; } diff --git a/tests/Remove-CosmosDbRecord.Tests.ps1 b/tests/Remove-CosmosDbRecord.Tests.ps1 index 8ce4617..96e608c 100644 --- a/tests/Remove-CosmosDbRecord.Tests.ps1 +++ b/tests/Remove-CosmosDbRecord.Tests.ps1 @@ -5,6 +5,7 @@ InModuleScope cosmos-db { Describe "Remove-CosmosDbRecord" { BeforeEach { Use-CosmosDbInternalFlag -EnableCaching $false + Use-CosmosDbReadonlyKeys -Disable . $PSScriptRoot\Utils.ps1 @@ -28,7 +29,7 @@ InModuleScope cosmos-db { $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$MOCK_RECORD_ID" } - Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $apiUriRecordId=$MOCK_RECORD_ID, $partitionKey=$MOCK_RECORD_ID) { + Function VerifyInvokeCosmosDbApiRequest($verb, $url, $body, $headers, $apiUriRecordId = $MOCK_RECORD_ID, $partitionKey = $MOCK_RECORD_ID) { $verb | Should -Be "delete" $url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$apiUriRecordId" $body | Should -Be $null @@ -51,6 +52,12 @@ InModuleScope cosmos-db { } } + It "Should throw in read only mode" { + Use-CosmosDbReadonlyKeys + + { Remove-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION -RecordId $MOCK_RECORD_ID } | Should -Throw "Operation not allowed in readonly mode" + } + It "Sends correct request with default partition key" { $response = @{ StatusCode = 200; @@ -172,7 +179,7 @@ InModuleScope cosmos-db { It "Url encodes the record id in the API url" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $testRecordId = "MOCK/RECORD/ID" @@ -205,7 +212,7 @@ InModuleScope cosmos-db { It "Url encodes the record id in the API url from an input object" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $testRecordId = "MOCK/RECORD/ID" diff --git a/tests/Update-CosmosDbRecord.Tests.ps1 b/tests/Update-CosmosDbRecord.Tests.ps1 index f8c846f..87ffefb 100644 --- a/tests/Update-CosmosDbRecord.Tests.ps1 +++ b/tests/Update-CosmosDbRecord.Tests.ps1 @@ -5,6 +5,7 @@ InModuleScope cosmos-db { Describe "Update-CosmosDbRecord" { BeforeEach { Use-CosmosDbInternalFlag -EnableCaching $false + Use-CosmosDbReadonlyKeys -Disable . $PSScriptRoot\Utils.ps1 @@ -20,8 +21,7 @@ InModuleScope cosmos-db { $MOCK_AUTH_HEADER = "MockAuthHeader" - Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now, $expectedId=$MOCK_RECORD_ID) - { + Function VerifyGetAuthHeader($ResourceGroup, $SubscriptionId, $Database, $verb, $resourceType, $resourceUrl, $now, $expectedId = $MOCK_RECORD_ID) { $ResourceGroup | Should -Be $MOCK_RG $SubscriptionId | Should -Be $MOCK_SUB @@ -30,8 +30,7 @@ InModuleScope cosmos-db { $resourceUrl | Should -Be "dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedId" } - Function VerifyInvokeCosmosDbApiRequest($verb, $url, $actualBody, $expectedBody, $headers, $expectedId=$MOCK_RECORD_ID, $expectedPartitionKey=$null, $enforceOptimisticConcurrency=$true) - { + Function VerifyInvokeCosmosDbApiRequest($verb, $url, $actualBody, $expectedBody, $headers, $expectedId = $MOCK_RECORD_ID, $expectedPartitionKey = $null, $enforceOptimisticConcurrency = $true) { $verb | Should -Be "put" $url | Should -Be "https://$MOCK_DB.documents.azure.com/dbs/$MOCK_CONTAINER/colls/$MOCK_COLLECTION/docs/$expectedId" @@ -43,7 +42,8 @@ InModuleScope cosmos-db { if ($EnforceOptimisticConcurrency) { $expectedHeaders = Get-CommonHeaders -now $global:capturedNow -encodedAuthString $MOCK_AUTH_HEADER -PartitionKey $expectedPartitionKey -Etag $MOCK_ETAG - } else { + } + else { $expectedHeaders = Get-CommonHeaders -now $global:capturedNow -encodedAuthString $MOCK_AUTH_HEADER -PartitionKey $expectedPartitionKey } AssertHashtablesEqual $expectedHeaders $headers @@ -60,16 +60,29 @@ InModuleScope cosmos-db { } } + It "Should throw in read only mode" { + Use-CosmosDbReadonlyKeys + + $payload = @{ + id = $MOCK_RECORD_ID; + key1 = "value1"; + key2 = 2; + "_etag" = $MOCK_ETAG; + } + + { $payload | Update-CosmosDbRecord -ResourceGroup $MOCK_RG -SubscriptionId $MOCK_SUB -Database $MOCK_DB -Container $MOCK_CONTAINER -Collection $MOCK_COLLECTION } | Should -Throw "Operation not allowed in readonly mode" + } + It "Sends correct request with default partition key" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $payload = @{ - id = $MOCK_RECORD_ID; - key1 = "value1"; - key2 = 2; + id = $MOCK_RECORD_ID; + key1 = "value1"; + key2 = 2; "_etag" = $MOCK_ETAG; } @@ -91,13 +104,13 @@ InModuleScope cosmos-db { It "Sends correct request with custom explicit partition key" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $payload = @{ - id = $MOCK_RECORD_ID; - key1 = "value1"; - key2 = 2; + id = $MOCK_RECORD_ID; + key1 = "value1"; + key2 = 2; "_etag" = $MOCK_ETAG; } @@ -121,13 +134,13 @@ InModuleScope cosmos-db { It "Sends correct request with custom partition key callback" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $payload = @{ - id = $MOCK_RECORD_ID; - key1 = "value1"; - key2 = 2; + id = $MOCK_RECORD_ID; + key1 = "value1"; + key2 = 2; "_etag" = $MOCK_ETAG; } @@ -158,13 +171,13 @@ InModuleScope cosmos-db { It "Optimistic concurrency can be disabled" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $payload = @{ - id = $MOCK_RECORD_ID; - key1 = "value1"; - key2 = 2; + id = $MOCK_RECORD_ID; + key1 = "value1"; + key2 = 2; "_etag" = $MOCK_ETAG; } @@ -230,7 +243,7 @@ InModuleScope cosmos-db { $response = @{ StatusCode = 200; - Content = $global:idx; + Content = $global:idx; } $global:expectedResponses += $response @@ -250,7 +263,7 @@ InModuleScope cosmos-db { It "Url encodes the record id in the API url" { $response = @{ StatusCode = 200; - Content = "{}" + Content = "{}" } $testRecordId = "MOCK/RECORD/ID" @@ -258,7 +271,7 @@ InModuleScope cosmos-db { $expectedAuthHeaderRecordId = $testRecordId # The id in the auth header should not be encoded $payload = @{ - id = $testRecordId; + id = $testRecordId; "_etag" = $MOCK_ETAG; } @@ -294,9 +307,9 @@ InModuleScope cosmos-db { $recordResponse = [PSCustomObject]@{} $payload = @{ - id = $MOCK_RECORD_ID; - key1 = "value1"; - key2 = 2; + id = $MOCK_RECORD_ID; + key1 = "value1"; + key2 = 2; "_etag" = $MOCK_ETAG; }