Skip to content

Commit 20ae5be

Browse files
authored
MI Live Tests (Azure#37075)
* adding mi test coverage and fixing headers to be sent the same for any authentication method * credential async * fixed live tests * turn on federated auth * added service connection * fixed live tests * added rbac permissions * added more mi test coverage with query * fix live tests: * fixing live tests by removing forbidden check because accounts have roles to do this operations * remove unnecessary imports * removed internal roles so that customers can run tests * removed internal roles so that customers can run tests * use built in role definition instead of custom role definition id * remove unused import * reacting to feedback
1 parent 34bb8e2 commit 20ae5be

File tree

7 files changed

+182
-27
lines changed

7 files changed

+182
-27
lines changed

sdk/cosmos/azure-cosmos/azure/cosmos/_base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,8 @@ def GetHeaders( # pylint: disable=too-many-statements,too-many-branches
248248
if options.get("priorityLevel"):
249249
headers[http_constants.HttpHeaders.PriorityLevel] = options["priorityLevel"]
250250

251-
if cosmos_client_connection.master_key:
252-
# formatdate guarantees RFC 1123 date format regardless of current locale
253-
headers[http_constants.HttpHeaders.XDate] = formatdate(timeval=None, localtime=False, usegmt=True)
251+
# formatdate guarantees RFC 1123 date format regardless of current locale
252+
headers[http_constants.HttpHeaders.XDate] = formatdate(timeval=None, localtime=False, usegmt=True)
254253

255254
if cosmos_client_connection.master_key or cosmos_client_connection.resource_tokens:
256255
resource_type = _internal_resourcetype(resource_type)

sdk/cosmos/azure-cosmos/test/test_aad.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import azure.cosmos.cosmos_client as cosmos_client
1414
import test_config
15-
from azure.cosmos import exceptions, DatabaseProxy, ContainerProxy
15+
from azure.cosmos import DatabaseProxy, ContainerProxy, exceptions
1616

1717

1818
def _remove_padding(encoded_string):
@@ -93,21 +93,15 @@ class TestAAD(unittest.TestCase):
9393
configs = test_config.TestConfig
9494
host = configs.host
9595
masterKey = configs.masterKey
96+
credential = CosmosEmulatorCredential() if configs.is_emulator else configs.credential
9697

9798
@classmethod
9899
def setUpClass(cls):
99-
cls.client = cosmos_client.CosmosClient(cls.host, cls.masterKey)
100+
cls.client = cosmos_client.CosmosClient(cls.host, cls.credential)
100101
cls.database = cls.client.get_database_client(cls.configs.TEST_DATABASE_ID)
101102
cls.container = cls.database.get_container_client(cls.configs.TEST_SINGLE_PARTITION_CONTAINER_ID)
102103

103-
def test_emulator_aad_credentials(self):
104-
if self.host != 'https://localhost:8081/':
105-
print("This test is only configured to run on the emulator, skipping now.")
106-
return
107-
108-
aad_client = cosmos_client.CosmosClient(self.host, CosmosEmulatorCredential())
109-
# Do any R/W data operations with your authorized AAD client
110-
104+
def test_aad_credentials(self):
111105
print("Container info: " + str(self.container.read()))
112106
self.container.create_item(get_test_item(0))
113107
print("Point read result: " + str(self.container.read_item(item='Item_0', partition_key='pk')))
@@ -118,7 +112,7 @@ def test_emulator_aad_credentials(self):
118112

119113
# Attempting to do management operations will return a 403 Forbidden exception
120114
try:
121-
aad_client.delete_database(self.configs.TEST_DATABASE_ID)
115+
self.client.delete_database(self.configs.TEST_DATABASE_ID)
122116
except exceptions.CosmosHttpResponseError as e:
123117
assert e.status_code == 403
124118
print("403 error assertion success")
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# The MIT License (MIT)
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
4+
import base64
5+
import json
6+
import time
7+
import unittest
8+
from io import StringIO
9+
10+
import pytest
11+
from azure.core.credentials import AccessToken
12+
13+
import test_config
14+
from azure.cosmos import exceptions
15+
from azure.cosmos.aio import CosmosClient, DatabaseProxy, ContainerProxy
16+
17+
18+
def _remove_padding(encoded_string):
19+
while encoded_string.endswith("="):
20+
encoded_string = encoded_string[0:len(encoded_string) - 1]
21+
22+
return encoded_string
23+
24+
25+
def get_test_item(num):
26+
test_item = {
27+
'pk': 'pk',
28+
'id': 'Item_' + str(num),
29+
'test_object': True,
30+
'lastName': 'Smith'
31+
}
32+
return test_item
33+
34+
35+
class CosmosEmulatorCredential(object):
36+
37+
async def get_token(self, *scopes, **kwargs):
38+
# type: (*str, **Any) -> AccessToken
39+
"""Request an access token for the emulator. Based on Azure Core's Access Token Credential.
40+
41+
This method is called automatically by Azure SDK clients.
42+
43+
:param str scopes: desired scopes for the access token. This method requires at least one scope.
44+
:rtype: :class:`azure.core.credentials.AccessToken`
45+
:raises CredentialUnavailableError: the credential is unable to attempt authentication because it lacks
46+
required data, state, or platform support
47+
:raises ~azure.core.exceptions.ClientAuthenticationError: authentication failed. The error's ``message``
48+
attribute gives a reason.
49+
"""
50+
aad_header_cosmos_emulator = "{\"typ\":\"JWT\",\"alg\":\"RS256\",\"x5t\":\"" \
51+
"CosmosEmulatorPrimaryMaster\",\"kid\":\"CosmosEmulatorPrimaryMaster\"}"
52+
53+
aad_claim_cosmos_emulator_format = {"aud": "https://localhost.localhost",
54+
"iss": "https://sts.fake-issuer.net/7b1999a1-dfd7-440e-8204-00170979b984",
55+
"iat": int(time.time()), "nbf": int(time.time()),
56+
"exp": int(time.time() + 7200), "aio": "", "appid": "localhost",
57+
"appidacr": "1", "idp": "https://localhost:8081/",
58+
"oid": "96313034-4739-43cb-93cd-74193adbe5b6", "rh": "", "sub": "localhost",
59+
"tid": "EmulatorFederation", "uti": "", "ver": "1.0",
60+
"scp": "user_impersonation",
61+
"groups": ["7ce1d003-4cb3-4879-b7c5-74062a35c66e",
62+
"e99ff30c-c229-4c67-ab29-30a6aebc3e58",
63+
"5549bb62-c77b-4305-bda9-9ec66b85d9e4",
64+
"c44fd685-5c58-452c-aaf7-13ce75184f65",
65+
"be895215-eab5-43b7-9536-9ef8fe130330"]}
66+
67+
emulator_key = test_config.TestConfig.masterKey
68+
69+
first_encoded_bytes = base64.urlsafe_b64encode(aad_header_cosmos_emulator.encode("utf-8"))
70+
first_encoded_padded = str(first_encoded_bytes, "utf-8")
71+
first_encoded = _remove_padding(first_encoded_padded)
72+
73+
str_io_obj = StringIO()
74+
json.dump(aad_claim_cosmos_emulator_format, str_io_obj)
75+
aad_claim_cosmos_emulator_format_string = str(str_io_obj.getvalue()).replace(" ", "")
76+
second = aad_claim_cosmos_emulator_format_string
77+
second_encoded_bytes = base64.urlsafe_b64encode(second.encode("utf-8"))
78+
second_encoded_padded = str(second_encoded_bytes, "utf-8")
79+
second_encoded = _remove_padding(second_encoded_padded)
80+
81+
emulator_key_encoded_bytes = base64.urlsafe_b64encode(emulator_key.encode("utf-8"))
82+
emulator_key_encoded_padded = str(emulator_key_encoded_bytes, "utf-8")
83+
emulator_key_encoded = _remove_padding(emulator_key_encoded_padded)
84+
85+
return AccessToken(first_encoded + "." + second_encoded + "." + emulator_key_encoded, int(time.time() + 7200))
86+
87+
88+
@pytest.mark.cosmosEmulator
89+
class TestAADAsync(unittest.TestCase):
90+
client: CosmosClient = None
91+
database: DatabaseProxy = None
92+
container: ContainerProxy = None
93+
configs = test_config.TestConfig
94+
host = configs.host
95+
masterKey = configs.masterKey
96+
credential = CosmosEmulatorCredential() if configs.is_emulator else configs.credential_async
97+
98+
@classmethod
99+
async def setUpClass(cls):
100+
cls.client = CosmosClient(cls.host, cls.credential)
101+
cls.database = cls.client.get_database_client(cls.configs.TEST_DATABASE_ID)
102+
cls.container = cls.database.get_container_client(cls.configs.TEST_SINGLE_PARTITION_CONTAINER_ID)
103+
104+
async def test_aad_credentials_async(self):
105+
# Do any R/W data operations with your authorized AAD client
106+
107+
print("Container info: " + str(self.container.read()))
108+
await self.container.create_item(get_test_item(0))
109+
print("Point read result: " + str(await self.container.read_item(item='Item_0', partition_key='pk')))
110+
query_results = list(await self.container.query_items(query='select * from c', partition_key='pk'))
111+
assert len(query_results) == 1
112+
print("Query result: " + str(query_results[0]))
113+
await self.container.delete_item(item='Item_0', partition_key='pk')
114+
115+
# Attempting to do management operations will return a 403 Forbidden exception
116+
try:
117+
await self.client.delete_database(self.configs.TEST_DATABASE_ID)
118+
except exceptions.CosmosHttpResponseError as e:
119+
assert e.status_code == 403
120+
print("403 error assertion success")
121+
122+
123+
if __name__ == "__main__":
124+
unittest.main()

sdk/cosmos/azure-cosmos/test/test_config.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from azure.cosmos.cosmos_client import CosmosClient
1313
from azure.cosmos.http_constants import StatusCodes
1414
from azure.cosmos.partition_key import PartitionKey
15+
from devtools_testutils.azure_recorded_testcase import get_credential
16+
from devtools_testutils.helpers import is_live
1517

1618
try:
1719
import urllib3
@@ -22,14 +24,19 @@
2224

2325

2426
class TestConfig(object):
27+
local_host = 'https://localhost:8081/'
2528
# [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Cosmos DB Emulator Key")]
2629
masterKey = os.getenv('ACCOUNT_KEY',
2730
'C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==')
28-
host = os.getenv('ACCOUNT_HOST', 'https://localhost:8081/')
31+
host = os.getenv('ACCOUNT_HOST', local_host)
2932
connection_str = os.getenv('ACCOUNT_CONNECTION_STR', 'AccountEndpoint={};AccountKey={};'.format(host, masterKey))
3033

3134
connectionPolicy = documents.ConnectionPolicy()
3235
connectionPolicy.DisableSSLVerification = True
36+
is_emulator = host == local_host
37+
is_live._cache = True if not is_emulator else False
38+
credential = masterKey if is_emulator else get_credential()
39+
credential_async = masterKey if is_emulator else get_credential(is_async=True)
3340

3441
global_host = os.getenv('GLOBAL_ACCOUNT_HOST', host)
3542
write_location_host = os.getenv('WRITE_LOCATION_HOST', host)

sdk/cosmos/azure-cosmos/test/test_query.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,14 @@ class TestQuery(unittest.TestCase):
2727
client: cosmos_client.CosmosClient = None
2828
config = test_config.TestConfig
2929
host = config.host
30-
masterKey = config.masterKey
3130
connectionPolicy = config.connectionPolicy
3231
TEST_DATABASE_ID = config.TEST_DATABASE_ID
32+
is_emulator = config.is_emulator
33+
credential = config.credential
3334

3435
@classmethod
3536
def setUpClass(cls):
36-
if (cls.masterKey == '[YOUR_KEY_HERE]' or
37-
cls.host == '[YOUR_ENDPOINT_HERE]'):
38-
raise Exception(
39-
"You must specify your Azure Cosmos account values for "
40-
"'masterKey' and 'host' at the top of this class to run the "
41-
"tests.")
42-
43-
cls.client = cosmos_client.CosmosClient(cls.host, cls.masterKey)
37+
cls.client = cosmos_client.CosmosClient(cls.host, cls.credential)
4438
cls.created_db = cls.client.get_database_client(cls.TEST_DATABASE_ID)
4539
if cls.host == "https://localhost:8081/":
4640
os.environ["AZURE_COSMOS_DISABLE_NON_STREAMING_ORDER_BY"] = "True"

sdk/cosmos/test-resources.bicep

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ param enableMultipleRegions bool = false
1313
param location string = resourceGroup().location
1414

1515
@description('The api version to be used by Bicep to create resources')
16-
param apiVersion string = '2020-04-01'
16+
param apiVersion string = '2023-04-15'
17+
18+
@description('The principal to assign the role to. This is application object id.')
19+
param testApplicationOid string
1720

1821
var accountName = toLower(baseName)
1922
var resourceId = cosmosAccount.id
@@ -40,8 +43,11 @@ var multiRegionConfiguration = [
4043
}
4144
]
4245
var locationsConfiguration = (enableMultipleRegions ? multiRegionConfiguration : singleRegionConfiguration)
46+
var roleDefinitionId = guid(baseName, 'roleDefinitionId')
47+
var roleAssignmentId = guid(baseName, 'roleAssignmentId')
48+
var roleDefinitionName = 'ExpandedRbacActions'
4349

44-
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2020-04-01' = {
50+
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = {
4551
name: toLower(accountName)
4652
location: location
4753
kind: 'GlobalDocumentDB'
@@ -64,6 +70,37 @@ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2020-04-01' = {
6470
}
6571
}
6672

73+
resource accountName_roleDefinitionId 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2023-04-15' = {
74+
parent: cosmosAccount
75+
name: roleDefinitionId
76+
properties: {
77+
roleName: roleDefinitionName
78+
type: 'CustomRole'
79+
assignableScopes: [
80+
cosmosAccount.id
81+
]
82+
permissions: [
83+
{
84+
dataActions: [
85+
'Microsoft.DocumentDB/databaseAccounts/readMetadata'
86+
'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'
87+
'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
88+
]
89+
}
90+
]
91+
}
92+
}
93+
94+
resource accountName_roleAssignmentId 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-04-15' = {
95+
parent: cosmosAccount
96+
name: guid(resourceGroup().id, roleAssignmentId, testApplicationOid)
97+
properties: {
98+
roleDefinitionId: accountName_roleDefinitionId.id
99+
principalId: testApplicationOid
100+
scope: cosmosAccount.id
101+
}
102+
}
103+
67104

68105
output ACCOUNT_HOST string = reference(resourceId, apiVersion).documentEndpoint
69106
output ACCOUNT_KEY string = listKeys(resourceId, apiVersion).primaryMasterKey

sdk/cosmos/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ trigger: none
33
extends:
44
template: ../../eng/pipelines/templates/stages/archetype-sdk-tests.yml
55
parameters:
6-
UseFederatedAuth: false
76
Clouds: 'Cosmos_Public'
87
CloudConfig:
98
Cosmos_Public:
109
SubscriptionConfigurations:
1110
- $(sub-config-azure-cloud-test-resources)
1211
- $(sub-config-cosmos-azure-cloud-test-resources)
12+
ServiceConnection: azure-sdk-tests
1313
MaxParallel: 6
1414
BuildTargetingString: azure-cosmos
1515
ServiceDirectory: cosmos

0 commit comments

Comments
 (0)