diff --git a/eng/common/scripts/get-codeowners.ps1 b/eng/common/scripts/get-codeowners.ps1 new file mode 100644 index 000000000000..fdf8453aceeb --- /dev/null +++ b/eng/common/scripts/get-codeowners.ps1 @@ -0,0 +1,49 @@ +param ( + $TargetDirectory, # should be in relative form from root of repo. EG: sdk/servicebus + $RootDirectory, # ideally $(Build.SourcesDirectory) + $VsoVariable = "" # target devops output variable +) +$target = $TargetDirectory.ToLower().Trim("/") +$codeOwnersLocation = Join-Path $RootDirectory -ChildPath ".github/CODEOWNERS" +$ownedFolders = @{} + +if (!(Test-Path $codeOwnersLocation)) { + Write-Host "Unable to find CODEOWNERS file in target directory $RootDirectory" + exit 1 +} + +$codeOwnersContent = Get-Content $codeOwnersLocation + +foreach ($contentLine in $codeOwnersContent) { + if (-not $contentLine.StartsWith("#") -and $contentLine){ + $splitLine = $contentLine -split "\s+" + + # CODEOWNERS file can also have labels present after the owner aliases + # gh aliases start with @ in codeowners. don't pass on to API calls + $ownedFolders[$splitLine[0].ToLower().Trim("/")] = ($splitLine[1..$($splitLine.Length)] ` + | ? { $_.StartsWith("@") } ` + | % { return $_.substring(1) }) -join "," + } +} + +$results = $ownedFolders[$target] + +if ($results) { + Write-Host "Found a folder $results to match $target" + + if ($VsoVariable) { + $alreadyPresent = [System.Environment]::GetEnvironmentVariable($VsoVariable) + + if ($alreadyPresent) { + $results += ",$alreadyPresent" + } + Write-Host "##vso[task.setvariable variable=$VsoVariable;]$results" + } + + return $results +} +else { + Write-Host "Unable to match path $target in CODEOWNERS file located at $codeOwnersLocation." + Write-Host ($ownedFolders | ConvertTo-Json) + return "" +} diff --git a/eng/common/testproxy/test-proxy-tool.yml b/eng/common/testproxy/test-proxy-tool.yml index 9f24b0f0d527..fad23feb53b3 100644 --- a/eng/common/testproxy/test-proxy-tool.yml +++ b/eng/common/testproxy/test-proxy-tool.yml @@ -15,17 +15,11 @@ steps: Write-Host "##vso[task.setvariable variable=ASPNETCORE_Kestrel__Certificates__Default__Password]password" displayName: 'Configure Kestrel Environment Variables' - - task: UseDotNet@2 - displayName: "Use .NET Core SDK" - inputs: - packageType: sdk - version: 5.0.205 - - pwsh: | dotnet tool install azure.sdk.tools.testproxy ` --tool-path $(Build.BinariesDirectory)/test-proxy ` --add-source https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json ` - --version 1.0.0-dev.20210811.2 + --version 1.0.0-dev.20211104.2 displayName: "Install test-proxy" - pwsh: | @@ -37,11 +31,7 @@ steps: # nohup does NOT continue beyond the current session if you use it within powershell - bash: | - sudo nohup $(Build.BinariesDirectory)/test-proxy/test-proxy & + nohup $(Build.BinariesDirectory)/test-proxy/test-proxy & displayName: "Run the testproxy - linux/mac" condition: and(succeeded(), ne(variables['Agent.OS'],'Windows_NT')) workingDirectory: "${{ parameters.rootFolder }}" - - - pwsh: | - Write-Host "##vso[task.setvariable variable=PATH]$(OriginalPath)" - displayName: 'Restore .NET version by resetting path' \ No newline at end of file diff --git a/eng/pipelines/docindex.yml b/eng/pipelines/docindex.yml index 9d59564d6cc2..22f587a1e558 100644 --- a/eng/pipelines/docindex.yml +++ b/eng/pipelines/docindex.yml @@ -10,6 +10,7 @@ jobs: DocRepoLocation: $(Pipeline.Workspace)/docs DocRepoOwner: MicrosoftDocs DocRepoName: azure-docs-sdk-python + DocValidationImageId: azuresdkimages.azurecr.io/pyrefautocr:latest steps: # Docs CI uses Python 3.6.8 but that is not available on this image - task: UsePythonVersion@0 @@ -21,6 +22,12 @@ jobs: - pwsh: pip install --upgrade pip wheel setuptools displayName: Update python tools for package verification + # Pull and build the docker image. + - template: /eng/common/pipelines/templates/steps/docker-pull-image.yml + parameters: + ContainerRegistryClientId: $(azuresdkimages-cr-clientid) + ContainerRegistryClientSecret: $(azuresdkimages-cr-clientsecret) + ImageId: "$(DocValidationImageId)" # Sync docs repo onboarding files/folders - template: /eng/common/pipelines/templates/steps/sparse-checkout.yml parameters: @@ -38,7 +45,7 @@ jobs: inputs: pwsh: true filePath: eng/common/scripts/Update-DocsMsPackages.ps1 - arguments: -DocRepoLocation $(DocRepoLocation) + arguments: -DocRepoLocation $(DocRepoLocation) -ImageId '$(DocValidationImageId)' displayName: Update Docs Onboarding condition: and(succeeded(), or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Force.MainUpdate'], 'true'))) # Push changes to docs repo @@ -70,7 +77,7 @@ jobs: inputs: pwsh: true filePath: eng/common/scripts/Update-DocsMsPackages.ps1 - arguments: -DocRepoLocation $(DocRepoLocation) -PackageSourceOverride "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" + arguments: -DocRepoLocation $(DocRepoLocation) -PackageSourceOverride "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" -ImageId '$(DocValidationImageId)' displayName: Update Docs Onboarding for Daily branch - template: /eng/common/pipelines/templates/steps/git-push-changes.yml parameters: diff --git a/eng/scripts/Language-Settings.ps1 b/eng/scripts/Language-Settings.ps1 index 581f6570a1a9..eb8bdcae81f7 100644 --- a/eng/scripts/Language-Settings.ps1 +++ b/eng/scripts/Language-Settings.ps1 @@ -162,18 +162,43 @@ function Get-python-GithubIoDocIndex() } function ValidatePackage($packageName, $packageVersion, $workingDirectory) { - $packageExpression = "$packageName$packageVersion" - Write-Host "Validating $packageExpression" - - $installTargetFolder = Join-Path $workingDirectory $packageName - New-Item -ItemType Directory -Force -Path $installTargetFolder | Out-Null - # Add more validation by replicating as much of the docs CI process as # possible # https://github.com/Azure/azure-sdk-for-python/issues/20109 + if (!$ImageId) { + Write-Host "Validating using pip command directly on $packageName." + FallbackValidation -packageName "$packageName" -packageVersion "$packageVersion" -workingDirectory $workingDirectory + } + else { + Write-Host "Validating using $ImageId on $packageName." + DockerValidation -packageName "$packageName" -packageVersion "$packageVersion" + } +} +function DockerValidation($packageName, $packageVersion) { + $packageExpression = "$packageName==$packageVersion" + docker run -e TARGET_PACKAGE=$packageExpression -t $ImageId + # The docker exit codes: https://docs.docker.com/engine/reference/run/#exit-status + # If the docker failed because of docker itself instead of the application, + # we should skip the validation and keep the packages. + if ($LASTEXITCODE -eq 125 -Or $LASTEXITCODE -eq 126 -Or $LASTEXITCODE -eq 127) { + Write-Host $commandLine + LogWarning "The `docker` command does not work with exit code $LASTEXITCODE. Fall back to npm install $packageName directly." + FallbackValidation -packageName "$packageName" -packageVersion "$packageVersion" + } + elseif ($LASTEXITCODE -ne 0) { + Write-Host $commandLine + LogWarning "Package $($Package.name) ref docs validation failed." + return $false + } + return $true +} + +function FallbackValidation($packageName, $packageVersion, $workingDirectory) { + $installTargetFolder = Join-Path $workingDirectory $packageName + New-Item -ItemType Directory -Force -Path $installTargetFolder | Out-Null + $packageExpression = "$packageName$packageVersion" try { $pipInstallOutput = "" - $extraIndexUrl = " --extra-index-url=$PackageSourceOverride" if ($PackageSourceOverride) { Write-Host "pip install $packageExpression --no-cache-dir --target $installTargetFolder --extra-index-url=$PackageSourceOverride" $pipInstallOutput = pip ` diff --git a/scripts/devops_tasks/git_helper.py b/scripts/devops_tasks/git_helper.py index bcc646e79ec5..b480b3a0a067 100644 --- a/scripts/devops_tasks/git_helper.py +++ b/scripts/devops_tasks/git_helper.py @@ -23,7 +23,7 @@ 'azure-cosmos': '3.2.0', 'azure-servicebus': '0.50.3', 'azure-eventgrid': '1.3.0', - 'azure-schemaregistry-avroserializer': '1.0.0b2', + 'azure-schemaregistry-avroserializer': '1.0.0b3', 'azure-storage-blob-changefeed' : '12.0.0b2', 'azure-storage-file-datalake': '12.2.0b1', 'azure-communication-identity': '1.0.0', diff --git a/scripts/release_issue_status/main.py b/scripts/release_issue_status/main.py index ef8f6b640988..ec98923120cf 100644 --- a/scripts/release_issue_status/main.py +++ b/scripts/release_issue_status/main.py @@ -11,8 +11,7 @@ import reply_generator as rg from utils import update_issue_body, get_readme_and_output_folder, \ - get_python_pipelines, get_pipeline_url, auto_close_issue - + get_python_pipelines, get_pipeline_url, auto_close_issue _NULL = ' ' _FILE_OUT = 'release_issue_status.csv' @@ -23,6 +22,7 @@ logging.basicConfig(level=logging.INFO, format='[auto-reply log] - %(funcName)s[line:%(lineno)d] - %(levelname)s: %(message)s') + def my_print(cmd): print('==' + cmd + ' ==\n') @@ -39,12 +39,13 @@ def output_python_md(issue_status_python): file_out.write('| ------ | ------ | ------ | ------ | ------ | ------ | ------ | :-----: |\n') file_out.writelines([item.output_python() for item in sorted(issue_status_python, key=_key_select)]) + def output_csv(issue_status): with open(_FILE_OUT, 'w') as file_out: file_out.write('language,issue,author,package,created date,delay from created date,latest update time,' 'delay from latest update,status,bot advice\n') file_out.writelines([item.output() for item in sorted(issue_status, key=_key_select)]) - + class IssueStatus: link = _NULL @@ -75,7 +76,6 @@ def output(self): self.delay_from_latest_update, self.status, self.bot_advice) - def output_python(self): package = self.package.split('-')[-1] create_date = str(date.fromtimestamp(self.create_date).strftime('%m-%d')) @@ -85,12 +85,13 @@ def output_python(self): else: days_from_target = ' ' - return '| [#{}]({}) | {} | {} | {} | {} | {} | {} | {} |\n'.format(self.link.split('/')[-1], self.link, self.author, - package, self.assignee, self.bot_advice, - create_date, - target_date, - days_from_target - ) + return '| [#{}]({}) | {} | {} | {} | {} | {} | {} | {} |\n'.format(self.link.split('/')[-1], self.link, + self.author, + package, self.assignee, self.bot_advice, + create_date, + target_date, + days_from_target + ) def _extract(str_list, key_word): @@ -148,7 +149,8 @@ def auto_reply(item, request_repo, rest_repo, sdk_repo, duplicated_issue, python if 'auto-link' not in item.labels: item.issue_object.add_to_labels('auto-link') try: - package_name, readme_link, output_folder = update_issue_body(request_repo, rest_repo, item.issue_object.number) + package_name, readme_link, output_folder = update_issue_body(request_repo, rest_repo, + item.issue_object.number) logging.info("pkname, readme", package_name, readme_link) item.package = package_name key = ('Python', item.package) @@ -163,7 +165,7 @@ def auto_reply(item, request_repo, rest_repo, sdk_repo, duplicated_issue, python readme_link, output_folder = get_readme_and_output_folder(request_repo, rest_repo, item.issue_object.number) except Exception as e: logging.info('Issue: {} get pkname and output folder failed'.format(item.issue_object.number)) - item.bot_advice = 'failed to find Readme link and output folder. Please check !!' + item.bot_advice = 'failed to find Readme link and output folder!
' item.issue_object.add_to_labels('attention') logging.info(e) raise @@ -176,7 +178,7 @@ def auto_reply(item, request_repo, rest_repo, sdk_repo, duplicated_issue, python if 'Configured' in item.labels: item.issue_object.remove_from_labels('Configured') except Exception as e: - item.bot_advice = 'auto reply failed, Please intervene manually !!' + item.bot_advice = 'auto reply failed!
' logging.info('Error from auto reply') logging.info('Issue:{}'.format(item.issue_object.number)) logging.info(traceback.format_exc()) @@ -208,7 +210,8 @@ def main(): issue.author = item.user.login issue.package = _extract(item.body.split('\n'), 'azure-.*') issue.target_date = [x.split(':')[-1].strip() for x in item.body.split('\n') if 'Target release date' in x][0] - issue.days_from_target = int((time.mktime(time.strptime(issue.target_date, '%Y-%m-%d')) - time.time())/3600/24) + issue.days_from_target = int( + (time.mktime(time.strptime(issue.target_date, '%Y-%m-%d')) - time.time()) / 3600 / 24) issue.create_date = item.created_at.timestamp() issue.delay_from_create_date = int((time.time() - item.created_at.timestamp()) / 3600 / 24) issue.latest_update = item.updated_at.timestamp() @@ -220,10 +223,9 @@ def main(): issue.whether_author_comment = _whether_author_comment(item.get_comments()) issue.issue_object = item issue.labels = [label.name for label in item.labels] - issue.days_from_latest_commit = _latest_comment_time(item.get_comments(), issue.delay_from_create_date) + issue.days_from_latest_comment = _latest_comment_time(item.get_comments(), issue.delay_from_create_date) if item.assignee: issue.assignee = item.assignee.login - issue_status.append(issue) key = (issue.language, issue.package) duplicated_issue[key] = duplicated_issue.get(key, 0) + 1 @@ -239,51 +241,52 @@ def main(): # rule7: if delay from created date is over 15 days and owner never reply, remind owner to handle it. for item in issue_status: if item.language == 'Python': - issue_status_python.append(item) + assigner_repo = assigner_repoes[item.assignee] + item.issue_object = assigner_repo.get_issue(number=item.issue_object.number) + issue_status_python.append(item) if item.status == 'release': item.bot_advice = 'better to release asap.' elif (item.comment_num == 0 or 'Configured' in item.labels) and 'Python' in item.labels: - item.bot_advice = 'new issue and better to confirm quickly.' + item.bot_advice = 'new issue !
' if 'assigned' not in item.labels: time.sleep(0.1) assign_count = int(str(time.time())[-1]) % len(_PYTHON_SDK_ASSIGNEES) item.issue_object.remove_from_assignees(item.assignee) item.issue_object.add_to_assignees(_PYTHON_SDK_ASSIGNEES[assign_count]) - item.assignee=item.issue_object.assignee.login + item.assignee = item.issue_object.assignee.login item.issue_object.add_to_labels('assigned') try: auto_reply(item, request_repo, rest_repo, sdk_repo, duplicated_issue, python_piplines, assigner_repoes) except Exception as e: continue elif not item.author_latest_comment in _PYTHON_SDK_ADMINISTRATORS: - item.bot_advice = 'new comment for author.' - elif item.delay_from_latest_update >= 7: - item.bot_advice = 'delay for a long time ' + item.bot_advice = 'new comment.
' if item.comment_num > 1 and item.language == 'Python': try: auto_close_issue(request_repo, item) except Exception as e: item.bot_advice = 'auto-close failed, please check!' logging.info(f"=====issue: {item.issue_object.number}, {e}") - + if 'base-branch-attention' in item.labels: item.bot_advice = 'new version is 0.0.0, please check base branch! ' + item.bot_advice if abs(item.days_from_target) < 3: item.bot_advice += ' release date < 2 !
' - - if item.days_from_latest_commit >= 30 and item.language == 'Python' and '30days attention' not in item.labels: - item.issue_object.add_to_labels('30days attention') - item.issue_object.create_comment(f'hi @{item.author}, the issue is closed since there is no reply for a long time. Please reopen it if necessary or create new one.') + + if item.days_from_latest_comment >= 15 and item.language == 'Python' and '7days attention' in item.labels and item.days_from_target < 0: + item.issue_object.create_comment( + f'hi @{item.author}, the issue is closed since there is no reply for a long time. Please reopen it if necessary or create new one.') item.issue_object.edit(state='close') - elif item.days_from_latest_commit >= 15 and item.language == 'Python' and '15days attention' not in item.labels: - item.issue_object.create_comment(f'hi @{item.author}, this release-request has been delayed more than 15 days,' - ' please deal with it ASAP. We will close the issue if there is still no response after 15 days!') - item.issue_object.add_to_labels('15days attention') + elif item.days_from_latest_comment >= 7 and item.language == 'Python' and '7days attention' not in item.labels and item.days_from_target < 7: + item.issue_object.create_comment( + f'hi @{item.author}, this release-request has been delayed more than 7 days,' + ' please deal with it ASAP. We will close the issue if there is still no response after 7 days!') + item.issue_object.add_to_labels('7days attention') # judge whether there is duplicated issue for same package if item.package != _NULL and duplicated_issue.get((item.language, item.package)) > 1: - item.bot_advice = f'duplicated issue for {item.package}. ' + item.bot_advice + item.bot_advice = f'duplicated issue
' + item.bot_advice # output result output_python_md(issue_status_python) @@ -294,12 +297,11 @@ def main(): print_check('git commit -m \"update excel\"') print_check('git push -f origin HEAD') - # upload to storage account(it is created in advance) +# upload to storage account(it is created in advance) # blob = BlobClient.from_connection_string(conn_str=os.getenv('CONN_STR'), container_name=os.getenv('FILE'), # blob_name=_FILE_OUT) # with open(_FILE_OUT, 'rb') as data: # blob.upload_blob(data, overwrite=True) - if __name__ == '__main__': main() diff --git a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md index 44ba46485439..7b31788ac3b9 100644 --- a/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration/CHANGELOG.md @@ -1,13 +1,8 @@ # Release History -## 1.3.0 (Unreleased) - -### Features Added - -### Breaking Changes +## 1.3.0 (2021-11-09) ### Bugs Fixed - - Fix the issue that data was persisted according to an incorrect schema/in an incorrect format ([#20518](https://github.com/Azure/azure-sdk-for-python/issues/20518)) `SecretReferenceConfigurationSetting` in 1.2.0 used "secret_uri" rather than "uri" as the schema keywords which @@ -17,8 +12,6 @@ - Use 1.3.0+ for any `SecretReferenceConfigurationSetting` uses. - Call a get method for existing `SecretReferenceConfigurationSetting`s and set them back to correct the format. -### Other Changes - ## 1.2.0 (2021-07-06) ### Features Added * Adds `FeatureFlagConfigurationSetting` and `SecretReferenceConfigurationSetting` models diff --git a/sdk/cognitivelanguage/azure-ai-language-conversations/CHANGELOG.md b/sdk/cognitivelanguage/azure-ai-language-conversations/CHANGELOG.md index a136c4c50b05..190fa5691ccd 100644 --- a/sdk/cognitivelanguage/azure-ai-language-conversations/CHANGELOG.md +++ b/sdk/cognitivelanguage/azure-ai-language-conversations/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 1.0.0b2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 1.0.0b1 (2021-11-03) ### Features Added diff --git a/sdk/cognitivelanguage/azure-ai-language-conversations/azure/ai/language/conversations/_version.py b/sdk/cognitivelanguage/azure-ai-language-conversations/azure/ai/language/conversations/_version.py index e5754a47ce68..dfa6ee022f15 100644 --- a/sdk/cognitivelanguage/azure-ai-language-conversations/azure/ai/language/conversations/_version.py +++ b/sdk/cognitivelanguage/azure-ai-language-conversations/azure/ai/language/conversations/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "1.0.0b1" +VERSION = "1.0.0b2" diff --git a/sdk/cognitivelanguage/azure-ai-language-questionanswering/CHANGELOG.md b/sdk/cognitivelanguage/azure-ai-language-questionanswering/CHANGELOG.md index f01da3820557..597044c35da0 100644 --- a/sdk/cognitivelanguage/azure-ai-language-questionanswering/CHANGELOG.md +++ b/sdk/cognitivelanguage/azure-ai-language-questionanswering/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 1.0.1 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 1.0.0 (2021-11-03) * We are now targeting service version `2021-10-01` diff --git a/sdk/cognitivelanguage/azure-ai-language-questionanswering/azure/ai/language/questionanswering/_version.py b/sdk/cognitivelanguage/azure-ai-language-questionanswering/azure/ai/language/questionanswering/_version.py index c47f66669f1b..961c76eb77c1 100644 --- a/sdk/cognitivelanguage/azure-ai-language-questionanswering/azure/ai/language/questionanswering/_version.py +++ b/sdk/cognitivelanguage/azure-ai-language-questionanswering/azure/ai/language/questionanswering/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "1.0.0" +VERSION = "1.0.1" diff --git a/sdk/communication/azure-communication-identity/CHANGELOG.md b/sdk/communication/azure-communication-identity/CHANGELOG.md index 8265a1818369..07f170020251 100644 --- a/sdk/communication/azure-communication-identity/CHANGELOG.md +++ b/sdk/communication/azure-communication-identity/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 1.1.0b2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 1.1.0b1 (2021-11-09) ### Features Added - Added support for Microsoft 365 Teams identities diff --git a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py index 7257fd834cec..fac699b2663b 100644 --- a/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py +++ b/sdk/communication/azure-communication-identity/azure/communication/identity/_version.py @@ -4,6 +4,6 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "1.1.0b1" +VERSION = "1.1.0b2" SDK_MONIKER = "communication-identity/{}".format(VERSION) # type: str diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 213564f6755c..ec1e97608c35 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -6,6 +6,8 @@ ### Breaking Changes +- Sync stream downloading now raises `azure.core.exceptions.DecodeError` rather than `requests.exceptions.ContentDecodingError` + ### Bugs Fixed ### Other Changes diff --git a/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py b/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py index ab9807e7bdc0..3896ad825d02 100644 --- a/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py +++ b/sdk/core/azure-core/azure/core/pipeline/transport/_requests_basic.py @@ -29,7 +29,7 @@ import urllib3 # type: ignore from urllib3.util.retry import Retry # type: ignore from urllib3.exceptions import ( - DecodeError, ReadTimeoutError, ProtocolError + DecodeError as CoreDecodeError, ReadTimeoutError, ProtocolError ) import requests @@ -39,6 +39,7 @@ ServiceResponseError, IncompleteReadError, HttpResponseError, + DecodeError ) from . import HttpRequest # pylint: disable=unused-import @@ -71,11 +72,11 @@ def _read_raw_stream(response, chunk_size=1): for chunk in response.raw.stream(chunk_size, decode_content=False): yield chunk except ProtocolError as e: - raise requests.exceptions.ChunkedEncodingError(e) - except DecodeError as e: - raise requests.exceptions.ContentDecodingError(e) + raise ServiceResponseError(e, error=e) + except CoreDecodeError as e: + raise DecodeError(e, error=e) except ReadTimeoutError as e: - raise requests.exceptions.ConnectionError(e) + raise ServiceRequestError(e, error=e) else: # Standard file-like object. while True: @@ -174,6 +175,8 @@ def __next__(self): raise StopIteration() except requests.exceptions.StreamConsumedError: raise + except requests.exceptions.ContentDecodingError as err: + raise DecodeError(err, error=err) except requests.exceptions.ChunkedEncodingError as err: msg = err.__str__() if 'IncompleteRead' in msg: diff --git a/sdk/core/azure-core/tests/test_streaming.py b/sdk/core/azure-core/tests/test_streaming.py index 87e51851722f..17f9d82f612e 100644 --- a/sdk/core/azure-core/tests/test_streaming.py +++ b/sdk/core/azure-core/tests/test_streaming.py @@ -97,7 +97,6 @@ def test_compress_compressed_no_header(http_request): @pytest.mark.parametrize("http_request", HTTP_REQUESTS) def test_decompress_plain_header(http_request): # expect error - import requests account_name = "coretests" account_url = "https://{}.blob.core.windows.net".format(account_name) url = "https://{}.blob.core.windows.net/tests/test_with_header.txt".format(account_name) @@ -109,7 +108,7 @@ def test_decompress_plain_header(http_request): try: content = b"".join(list(data)) assert False - except (requests.exceptions.ContentDecodingError, DecodeError): + except DecodeError: pass @pytest.mark.parametrize("http_request", HTTP_REQUESTS) diff --git a/sdk/eventgrid/azure-eventgrid/CHANGELOG.md b/sdk/eventgrid/azure-eventgrid/CHANGELOG.md index e51681f0958f..6f71310deefd 100644 --- a/sdk/eventgrid/azure-eventgrid/CHANGELOG.md +++ b/sdk/eventgrid/azure-eventgrid/CHANGELOG.md @@ -1,17 +1,11 @@ # Release History -## 4.7.0 (Unreleased) +## 4.7.0 (2021-11-09) ### Features Added - Added support for publishing native CNCF cloudevents (https://pypi.org/project/cloudevents/). -### Breaking Changes - -### Bugs Fixed - -### Other Changes - ## 4.6.0 (2021-10-05) ### Features Added diff --git a/sdk/formrecognizer/azure-ai-formrecognizer/CHANGELOG.md b/sdk/formrecognizer/azure-ai-formrecognizer/CHANGELOG.md index 2e9e95109d6b..63bdfba01fd1 100644 --- a/sdk/formrecognizer/azure-ai-formrecognizer/CHANGELOG.md +++ b/sdk/formrecognizer/azure-ai-formrecognizer/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 3.2.0b3 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 3.2.0b2 (2021-11-09) ### Features Added diff --git a/sdk/formrecognizer/azure-ai-formrecognizer/azure/ai/formrecognizer/_version.py b/sdk/formrecognizer/azure-ai-formrecognizer/azure/ai/formrecognizer/_version.py index eb9792017c53..78189923471e 100644 --- a/sdk/formrecognizer/azure-ai-formrecognizer/azure/ai/formrecognizer/_version.py +++ b/sdk/formrecognizer/azure-ai-formrecognizer/azure/ai/formrecognizer/_version.py @@ -4,4 +4,4 @@ # Licensed under the MIT License. # ------------------------------------ -VERSION = "3.2.0b2" +VERSION = "3.2.0b3" diff --git a/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md b/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md index 5bbecd93b402..2b87d2e07414 100644 --- a/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md @@ -3,12 +3,15 @@ ## 4.1.0b2 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.1.0b1 (2021-09-09) diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py index f73bf01e0e89..905e81953480 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py @@ -6,7 +6,7 @@ from six.moves.urllib_parse import urlparse -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -14,7 +14,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-administration/setup.py b/sdk/keyvault/azure-keyvault-administration/setup.py index 0231b074702c..9016754edd4b 100644 --- a/sdk/keyvault/azure-keyvault-administration/setup.py +++ b/sdk/keyvault/azure-keyvault-administration/setup.py @@ -81,7 +81,7 @@ "azure.keyvault", ] ), - install_requires=["azure-common~=1.1", "azure-core<2.0.0,>=1.11.0", "msrest>=0.6.21", "six>=1.11.0"], + install_requires=["azure-common~=1.1", "azure-core<2.0.0,>=1.15.0", "msrest>=0.6.21", "six>=1.11.0"], extras_require={ ":python_version<'3.0'": ["azure-keyvault-nspkg"], ":python_version<'3.4'": ["enum34>=1.0.4"], diff --git a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md index f399e57e03c3..e187f020b952 100644 --- a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md @@ -3,12 +3,15 @@ ## 4.4.0b2 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.4.0b1 (2021-09-09) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py index 3329ce2df068..d8303b4b081d 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py @@ -9,7 +9,7 @@ import urlparse as parse # type: ignore from typing import TYPE_CHECKING -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -21,7 +21,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-certificates/setup.py b/sdk/keyvault/azure-keyvault-certificates/setup.py index e9d54c896c34..5eeceb33b647 100644 --- a/sdk/keyvault/azure-keyvault-certificates/setup.py +++ b/sdk/keyvault/azure-keyvault-certificates/setup.py @@ -83,7 +83,7 @@ ] ), install_requires=[ - "azure-core<2.0.0,>=1.7.0", + "azure-core<2.0.0,>=1.15.0", "msrest>=0.6.21", "azure-common~=1.1", ], diff --git a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md index 5260048522ec..87981d1e5558 100644 --- a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md @@ -3,6 +3,8 @@ ## 4.5.0b5 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes > These changes do not impact the API of stable versions such as 4.4.0. @@ -15,6 +17,7 @@ ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.5.0b4 (2021-10-07) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py index 3329ce2df068..d8303b4b081d 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py @@ -9,7 +9,7 @@ import urlparse as parse # type: ignore from typing import TYPE_CHECKING -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -21,7 +21,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-keys/setup.py b/sdk/keyvault/azure-keyvault-keys/setup.py index 3a12c3d53b5d..400544558a44 100644 --- a/sdk/keyvault/azure-keyvault-keys/setup.py +++ b/sdk/keyvault/azure-keyvault-keys/setup.py @@ -82,7 +82,7 @@ ] ), install_requires=[ - "azure-core<2.0.0,>=1.7.0", + "azure-core<2.0.0,>=1.15.0", "cryptography>=2.1.4", "msrest>=0.6.21", "azure-common~=1.1", diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_mhsm.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_mhsm.yaml new file mode 100644 index 000000000000..01d1a918d4e9 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_mhsm.yaml @@ -0,0 +1,173 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/create?api-version=7.3-preview + response: + body: + string: '' + headers: + cache-control: + - no-cache + content-length: + - '0' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + www-authenticate: + - Bearer authorization="https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://managedhsm.azure.net" + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-server-latency: + - '1' + status: + code: 401 + message: Unauthorized +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/create?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636415841,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636415841},"key":{"e":"AQAB","key_ops":["wrapKey","sign","verify","encrypt","decrypt","unwrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/818c3c2903cc06289eaacf1a2070d5bd","kty":"RSA-HSM","n":"ncexSVGCn8VKmtNlZJAtTYK80rexx2xO_v_WOFuqCz3VYxqy7bzU2dsL3PWp66SqdU-OEXJ3jfuvyk4JCO2uYf8QF-7kVKCeB0N93pqh-2hMi_mDVaT0iMpB-USuJA34K0tDsouDa1B86WlXDeh1OBMFM4H_tVgDwgwS-zHXfM0L2UmTjekXSj5XRG2vaUztlIxfYKRrw4lSi5sRLv7VXiS-HWZG4x1U9miyCKtPdqAWqkqZ9O5yOQZ-B0Hk5yeO8TWbfMgyUc61H8ga3p0KExiLJatSCNPaxeOzlf-Eqb2g5pEFR74Nk9d_HkvW0bWs1elcMxZPehG_aF76U3GYwQ"}}' + headers: + cache-control: + - no-cache + content-length: + - '741' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - westus + x-ms-server-latency: + - '393' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636415841,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636415841},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/818c3c2903cc06289eaacf1a2070d5bd","kty":"RSA-HSM","n":"ncexSVGCn8VKmtNlZJAtTYK80rexx2xO_v_WOFuqCz3VYxqy7bzU2dsL3PWp66SqdU-OEXJ3jfuvyk4JCO2uYf8QF-7kVKCeB0N93pqh-2hMi_mDVaT0iMpB-USuJA34K0tDsouDa1B86WlXDeh1OBMFM4H_tVgDwgwS-zHXfM0L2UmTjekXSj5XRG2vaUztlIxfYKRrw4lSi5sRLv7VXiS-HWZG4x1U9miyCKtPdqAWqkqZ9O5yOQZ-B0Hk5yeO8TWbfMgyUc61H8ga3p0KExiLJatSCNPaxeOzlf-Eqb2g5pEFR74Nk9d_HkvW0bWs1elcMxZPehG_aF76U3GYwQ"}}' + headers: + cache-control: + - no-cache + content-length: + - '741' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-build-version: + - 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - westus + x-ms-server-latency: + - '90' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636415841,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636415841},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/818c3c2903cc06289eaacf1a2070d5bd","kty":"RSA-HSM","n":"ncexSVGCn8VKmtNlZJAtTYK80rexx2xO_v_WOFuqCz3VYxqy7bzU2dsL3PWp66SqdU-OEXJ3jfuvyk4JCO2uYf8QF-7kVKCeB0N93pqh-2hMi_mDVaT0iMpB-USuJA34K0tDsouDa1B86WlXDeh1OBMFM4H_tVgDwgwS-zHXfM0L2UmTjekXSj5XRG2vaUztlIxfYKRrw4lSi5sRLv7VXiS-HWZG4x1U9miyCKtPdqAWqkqZ9O5yOQZ-B0Hk5yeO8TWbfMgyUc61H8ga3p0KExiLJatSCNPaxeOzlf-Eqb2g5pEFR74Nk9d_HkvW0bWs1elcMxZPehG_aF76U3GYwQ"}}' + headers: + cache-control: + - no-cache + content-length: + - '741' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-build-version: + - 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - westus + x-ms-server-latency: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_vault.yaml new file mode 100644 index 000000000000..c251debdac2a --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_vault.yaml @@ -0,0 +1,190 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/create?api-version=7.3-preview + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: + - no-cache + content-length: + - '97' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:24 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + www-authenticate: + - Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 401 + message: Unauthorized +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/create?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/fae15351fde54b7384e4145ab181b03f","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yP2jt3YHJ3Kc46lv-pZtzI0iZZkbsXVwmO-GAe3kBlPeyzbFI6oT7KeUuwtVW0410mOdOUGCGWVk_BoXW09s49ScUPz0JPA8Hyc64y3MgW2u8frHWLq6EJsB7c4Sjz0oK2HwdNqLgRsmQ4DdzoXp5os2NQ0qt_hB39VNR1RL9J_25xevw7VUGiQfOcEYwTKYYnhLoxK_j74oRbd42-Ecck8riN76kL--Sp-3bqNX2fx3HzQVr8Vo4_wL84Stj64gAAJZ3q4J53qNeUfx7e99qJvBFBICTWiQq5b3y0wkcAxk6Bhq_2MB_WnZReaZ3pCm-XhJJU9MqwQ1Phkgb8bv8Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636415845,"updated":1636415845,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '701' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:25 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/fae15351fde54b7384e4145ab181b03f","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yP2jt3YHJ3Kc46lv-pZtzI0iZZkbsXVwmO-GAe3kBlPeyzbFI6oT7KeUuwtVW0410mOdOUGCGWVk_BoXW09s49ScUPz0JPA8Hyc64y3MgW2u8frHWLq6EJsB7c4Sjz0oK2HwdNqLgRsmQ4DdzoXp5os2NQ0qt_hB39VNR1RL9J_25xevw7VUGiQfOcEYwTKYYnhLoxK_j74oRbd42-Ecck8riN76kL--Sp-3bqNX2fx3HzQVr8Vo4_wL84Stj64gAAJZ3q4J53qNeUfx7e99qJvBFBICTWiQq5b3y0wkcAxk6Bhq_2MB_WnZReaZ3pCm-XhJJU9MqwQ1Phkgb8bv8Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636415845,"updated":1636415845,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '701' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:25 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/fae15351fde54b7384e4145ab181b03f","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yP2jt3YHJ3Kc46lv-pZtzI0iZZkbsXVwmO-GAe3kBlPeyzbFI6oT7KeUuwtVW0410mOdOUGCGWVk_BoXW09s49ScUPz0JPA8Hyc64y3MgW2u8frHWLq6EJsB7c4Sjz0oK2HwdNqLgRsmQ4DdzoXp5os2NQ0qt_hB39VNR1RL9J_25xevw7VUGiQfOcEYwTKYYnhLoxK_j74oRbd42-Ecck8riN76kL--Sp-3bqNX2fx3HzQVr8Vo4_wL84Stj64gAAJZ3q4J53qNeUfx7e99qJvBFBICTWiQq5b3y0wkcAxk6Bhq_2MB_WnZReaZ3pCm-XhJJU9MqwQ1Phkgb8bv8Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636415845,"updated":1636415845,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '701' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:25 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_2016_10_01_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_2016_10_01_vault.yaml new file mode 100644 index 000000000000..eb3682928b17 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_2016_10_01_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:28 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/2fe8efc82a53428b9d5bbc5ea90dc276","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"6qDo5N0hCCRwO1IVj0XFfjFsJjno0YClIjURSlKFUOX7YIluWkw2o6RUKGyDQujcYpVj9Q8Y6NfeZn3quJUBIRHa02ir0ToghrJ9-X4c_hg8ijTt-Ag70WmkHOKvriZgVvbhu-9YwqBa_C1k65uulqz4TujXKakfcB5i4Z7RdGdxhrEdh2vRdgLu9UIv0Rn3eLb5bPjlNMpb44XiqXBP_TT_2TKSHZg2YRMx-jFNGqJAq8qb0PbM4dM8TODH76xV4_15bT49yTE_PuHA9jKYcC9H-rkPa-xhL6q9PFhKLEN4DX3oveYL0M-qBo8vkDCBkfeiHzUj7dIE_cEZ4erDgQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636131329,"updated":1636131329,"recoveryLevel":"Recoverable+Purgeable"}}' + headers: + cache-control: no-cache + content-length: '680' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:29 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_0_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_0_vault.yaml new file mode 100644 index 000000000000..f441f52f0bbe --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_0_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:31 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/940432f6b7744d0fa9122d895cb89a2c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"-fBZm71ITwyw2jgr9u422zbHE7zvY6T55146MlEwH4MFOvPxs_EdSbv25ScUGMdVP5lkn4xr64P0lQOjhI6m3nTbWvLFb81Cm5YW8Gt0YleMg_HKZgLqgdPBFDgHANLeioQCNGTkguTJ19L__8r7MhB9YuoQz0kp02-vK1ZaNNvo7lsqDMvBVcQnF-bQ0Wd-SG3oZ4Z_PRtKZMaB44WkHGbSpH8rZ2fJu8ckqSNIU6UsZokLZEy5WsDjndctrrjPFDKmMhTxePewqQ1JxL_I92F25FtW19uK9iCCsyYuBHma9_qjOCONEPNqv9GA3hDEmPqgB50jEnWbDQFzFj280Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636131331,"updated":1636131331,"recoveryLevel":"Recoverable+Purgeable"}}' + headers: + cache-control: no-cache + content-length: '680' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:31 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_1_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_1_vault.yaml new file mode 100644 index 000000000000..bae43223c033 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_1_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:34 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/885311947edd4eccbcb7265fb430acef","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"mEKr5BmEhUKeMQ1VT185F2qTEKcyu23WCX__Puze8iviw12lnQyCZ0S-tpOeR37JMJtNY40d4Yc5yCN-DEDeziWILoCrkJRVm3-LYD4p8m_ktjh-yPlG0WL1zNLYHm9G_OipsM0cUXlplEdEIA7Gl-5tCpkVr__yYv_jdDHoVyC8pLYsFkzWMvXXhFyujTUqHKjnMRR_jiMosC0Dc12LoNbM-WcXuhg-ihmomagI2pQ6_E_F1vywbhnqc-oO8KpCPuTbIKR3JnEhffCrOMORVelj6kvEQ4VuTgoY4uuocIJiCcU7UAU86HFDHjH7zkAu_H_e0Chgxp0en4cZxn4RXQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636131334,"updated":1636131334,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:34 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_mhsm.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_mhsm.yaml new file mode 100644 index 000000000000..0ee86866acd2 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_mhsm.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 + response: + body: + string: '' + headers: + cache-control: no-cache + content-length: '0' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + www-authenticate: Bearer authorization="https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://managedhsm.azure.net" + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-server-latency: '1' + status: + code: 401 + message: Unauthorized + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 + response: + body: + string: '{"attributes":{"created":1636131337,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636131337},"key":{"e":"AQAB","key_ops":["wrapKey","decrypt","encrypt","unwrapKey","sign","verify"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/fed58eedaeb804d1b59ab2bea1dfb796","kty":"RSA-HSM","n":"4UmgdeRIG5a4n9Yb0BazkZ1x4qmn0I8oX7E8lzUG1xnKTn0NkmXRgvjUvlB8pz8074rwS89FlXD3f2r2I7viPK8xqvDSlxWOTfT-5osAQcHBbFlRUd3DIgXDYkpojroOHQq9ygERVTl6yei-Dawq_D50fETMY4Hetjqbi1AhrkA8RM_T56aFzC5hL4WqVFQTZIpMsQTRXAVp598GMl1oMn8AvUKAsPhmRwgwLY8wWOoW6kN60xJPA9cR7QrZAtb6UDJOiPj6yXu4X7ceW4tKu3iVliyKNmZxSD272sCSJqaJlezZrw96Zc8VH3arIc9b-Ku8-Tw8Raxn_o9hHixD7w"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '666' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_vault.yaml new file mode 100644 index 000000000000..669f41bd0af4 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:39 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/7e4059bf824742cfb3fc1bfc75fac8cb","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yY5-Q2uVQjCdv9IJZT2DsJzkV6FolvKSu_1XN4rhN-11TMfZroiVCtBOLeeHTgPEVy9rEonytZihEZcaW2hZmZ-M1FI6WvYabMX4Lqayc8iPrZ6cRcU3ALANla2Rn6UWbrDfiZKJg4yZhJF3Aacl_CsE5NsFkkQpRiabKSU3s_df7zCxHr6xKHdZmwRvsDameyVkkGFchVxj3Ol6t37Z_MZXU24AYyH1tKNKcgcv3dpRP2g-fW91lOJsAsGk6IMLM-efKZc4iGIe6iHseNNQQE_2zBdyFOQpNwqqJV1wFTIjpLuQXpzNPIVxBebWW6RA1vq1L53ie1kr4kHK2fBcZQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636131340,"updated":1636131340,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:40 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_mhsm.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_mhsm.yaml new file mode 100644 index 000000000000..356c41e3422f --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_mhsm.yaml @@ -0,0 +1,120 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview + response: + body: + string: '' + headers: + cache-control: no-cache + content-length: '0' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + www-authenticate: Bearer authorization="https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://managedhsm.azure.net" + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-server-latency: '1' + status: + code: 401 + message: Unauthorized + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636416230,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636416230},"key":{"e":"AQAB","key_ops":["wrapKey","sign","verify","encrypt","decrypt","unwrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/b9455b5cd9a34d3b2c3a637e61a550f9","kty":"RSA-HSM","n":"r5y21Ndv_xZgYFpqLHejepFyjnoBIQQ8usaUK0d4K6YyEYVRb2MbokiZqvKoOQ3jcvpOfoFlikEYi207ykpi8ukRjiY-vD_MjjkN_iJRASZZ7Z_jP5NsqGanHLIwQGA5orTH_rRxNeGBmRx6lCxnUT2CKKOOTDEorQK9H1FhaWpekKhdfJDUOVKRy-SER46fRDd7lWwqAQ5uUJ9nP0JM1vF8zeaYXlU7b7TmH4dQsTFeEY9TcRJPCGEnkadHk17l6xHK17DTg9YrVN_T6TcALQLdevVb5wefIF6yHin_1_hwjcTG6WxDorZzP-XVlHplGAXoW_KvJZ_akW-chShkXQ"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '376' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636416230,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636416230},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/b9455b5cd9a34d3b2c3a637e61a550f9","kty":"RSA-HSM","n":"r5y21Ndv_xZgYFpqLHejepFyjnoBIQQ8usaUK0d4K6YyEYVRb2MbokiZqvKoOQ3jcvpOfoFlikEYi207ykpi8ukRjiY-vD_MjjkN_iJRASZZ7Z_jP5NsqGanHLIwQGA5orTH_rRxNeGBmRx6lCxnUT2CKKOOTDEorQK9H1FhaWpekKhdfJDUOVKRy-SER46fRDd7lWwqAQ5uUJ9nP0JM1vF8zeaYXlU7b7TmH4dQsTFeEY9TcRJPCGEnkadHk17l6xHK17DTg9YrVN_T6TcALQLdevVb5wefIF6yHin_1_hwjcTG6WxDorZzP-XVlHplGAXoW_KvJZ_akW-chShkXQ"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-build-version: 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '92' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636416230,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636416230},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/b9455b5cd9a34d3b2c3a637e61a550f9","kty":"RSA-HSM","n":"r5y21Ndv_xZgYFpqLHejepFyjnoBIQQ8usaUK0d4K6YyEYVRb2MbokiZqvKoOQ3jcvpOfoFlikEYi207ykpi8ukRjiY-vD_MjjkN_iJRASZZ7Z_jP5NsqGanHLIwQGA5orTH_rRxNeGBmRx6lCxnUT2CKKOOTDEorQK9H1FhaWpekKhdfJDUOVKRy-SER46fRDd7lWwqAQ5uUJ9nP0JM1vF8zeaYXlU7b7TmH4dQsTFeEY9TcRJPCGEnkadHk17l6xHK17DTg9YrVN_T6TcALQLdevVb5wefIF6yHin_1_hwjcTG6WxDorZzP-XVlHplGAXoW_KvJZ_akW-chShkXQ"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-build-version: 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '0' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_vault.yaml new file mode 100644 index 000000000000..840be1178954 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_vault.yaml @@ -0,0 +1,129 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:53 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/f875421ae9684ee89ffa78d33c4b917c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"uh7LV8qFwSDSazU05d4aArOVG7HK72oX8Qvdlq-5FJwPRbbCTU9ZX7BJcI7dnhvQHHpYnvZ54aHaNX4nrqYRJRKv9fgte_ZyGaaBqnUVd_sjf3f8US7O6ayPzhzV-F8x2ML12yeRuXbsquxG05D3_e7OtAm5aSwqHxPoIYKIjh4hI3FtpryM7ECA3op1M9QH4e-_3CGCSbLEomih0_FZ-WkebiHNryYJi2l02p-b-CNAEYlcRpRlZUm8Gt6ZtKJ-YvaT1uvPw83DSW6Q_LDjhojmswJBUYCtWszDjBbcUsi5dKD0Zo7--ANcC-66H-CnTM5cF5RRygj1N5l2DU8xyQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636416233,"updated":1636416233,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:53 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/f875421ae9684ee89ffa78d33c4b917c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"uh7LV8qFwSDSazU05d4aArOVG7HK72oX8Qvdlq-5FJwPRbbCTU9ZX7BJcI7dnhvQHHpYnvZ54aHaNX4nrqYRJRKv9fgte_ZyGaaBqnUVd_sjf3f8US7O6ayPzhzV-F8x2ML12yeRuXbsquxG05D3_e7OtAm5aSwqHxPoIYKIjh4hI3FtpryM7ECA3op1M9QH4e-_3CGCSbLEomih0_FZ-WkebiHNryYJi2l02p-b-CNAEYlcRpRlZUm8Gt6ZtKJ-YvaT1uvPw83DSW6Q_LDjhojmswJBUYCtWszDjBbcUsi5dKD0Zo7--ANcC-66H-CnTM5cF5RRygj1N5l2DU8xyQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636416233,"updated":1636416233,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:54 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/f875421ae9684ee89ffa78d33c4b917c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"uh7LV8qFwSDSazU05d4aArOVG7HK72oX8Qvdlq-5FJwPRbbCTU9ZX7BJcI7dnhvQHHpYnvZ54aHaNX4nrqYRJRKv9fgte_ZyGaaBqnUVd_sjf3f8US7O6ayPzhzV-F8x2ML12yeRuXbsquxG05D3_e7OtAm5aSwqHxPoIYKIjh4hI3FtpryM7ECA3op1M9QH4e-_3CGCSbLEomih0_FZ-WkebiHNryYJi2l02p-b-CNAEYlcRpRlZUm8Gt6ZtKJ-YvaT1uvPw83DSW6Q_LDjhojmswJBUYCtWszDjBbcUsi5dKD0Zo7--ANcC-66H-CnTM5cF5RRygj1N5l2DU8xyQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636416233,"updated":1636416233,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:54 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py index d4f1eff32af8..955141d98943 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py @@ -7,6 +7,7 @@ the challenge cache is global to the process. """ import functools +import os import time from uuid import uuid4 @@ -20,11 +21,53 @@ from azure.core.pipeline import Pipeline from azure.core.pipeline.policies import SansIOHTTPPolicy from azure.core.pipeline.transport import HttpRequest +from azure.identity import ClientSecretCredential +from azure.keyvault.keys import KeyClient from azure.keyvault.keys._shared import ChallengeAuthPolicy, HttpChallenge, HttpChallengeCache +from azure.keyvault.keys._shared.client_base import DEFAULT_VERSION import pytest from _shared.helpers import mock_response, Request, validating_transport +from _shared.test_case import KeyVaultTestCase +from _test_case import client_setup, get_decorator, KeysTestCase + + +only_default_version = get_decorator(api_versions=[DEFAULT_VERSION]) + + +class ChallengeAuthTests(KeysTestCase, KeyVaultTestCase): + def __init__(self, *args, **kwargs): + super(ChallengeAuthTests, self).__init__(*args, match_body=False, **kwargs) + + @only_default_version() + @client_setup + def test_multitenant_authentication(self, client, is_hsm, **kwargs): + if not self.is_live: + pytest.skip("This test is incompatible with vcrpy in playback") + + client_id = os.environ.get("KEYVAULT_CLIENT_ID") + client_secret = os.environ.get("KEYVAULT_CLIENT_SECRET") + if not (client_id and client_secret): + pytest.skip("Values for KEYVAULT_CLIENT_ID and KEYVAULT_CLIENT_SECRET are required") + + # we set up a client for this method to align with the async test, but we actually want to create a new client + # this new client should use a credential with an initially fake tenant ID and still succeed with a real request + credential = ClientSecretCredential(tenant_id=str(uuid4()), client_id=client_id, client_secret=client_secret) + vault_url = self.managed_hsm_url if is_hsm else self.vault_url + client = KeyClient(vault_url=vault_url, credential=credential) + + if self.is_live: + time.sleep(2) # to avoid throttling by the service + key_name = self.get_resource_name("multitenant-key") + key = client.create_rsa_key(key_name) + assert key.id + + # try making another request with the credential's token revoked + # the challenge policy should correctly request a new token for the correct tenant when a challenge is cached + client._client._config.authentication_policy._token = None + fetched_key = client.get_key(key_name) + assert key.id == fetched_key.id def empty_challenge_cache(fn): @@ -74,7 +117,8 @@ def test_challenge_cache(): def test_challenge_parsing(): - authority = "https://login.authority.net/tenant" + tenant = "tenant" + authority = "https://login.authority.net/{}".format(tenant) resource = "https://challenge.resource" challenge = HttpChallenge( "https://request.uri", challenge="Bearer authorization={}, resource={}".format(authority, resource) @@ -82,6 +126,7 @@ def test_challenge_parsing(): assert challenge.get_authorization_server() == authority assert challenge.get_resource() == resource + assert challenge.tenant_id == tenant @empty_challenge_cache @@ -111,7 +156,7 @@ def send(request): return Mock(status_code=200) raise ValueError("unexpected request") - def get_token(*scopes): + def get_token(*scopes, **_): assert len(scopes) == 1 assert scopes[0] == expected_scope return AccessToken(expected_token, 0) @@ -143,6 +188,56 @@ def get_token(*scopes): test_with_challenge(challenge_with_scope, scope) +@empty_challenge_cache +def test_tenant(): + """The policy's token requests should pass the parsed tenant ID from the challenge""" + + expected_content = b"a duck" + + def test_with_challenge(challenge, expected_tenant): + expected_token = "expected_token" + + class Requests: + count = 0 + + def send(request): + Requests.count += 1 + if Requests.count == 1: + # first request should be unauthorized and have no content + assert not request.body + assert request.headers["Content-Length"] == "0" + return challenge + elif Requests.count == 2: + # second request should be authorized according to challenge and have the expected content + assert request.headers["Content-Length"] + assert request.body == expected_content + assert expected_token in request.headers["Authorization"] + return Mock(status_code=200) + raise ValueError("unexpected request") + + def get_token(*_, **kwargs): + assert kwargs.get("tenant_id") == expected_tenant + return AccessToken(expected_token, 0) + + credential = Mock(get_token=Mock(wraps=get_token)) + pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=Mock(send=send)) + request = HttpRequest("POST", get_random_url()) + request.set_bytes_body(expected_content) + pipeline.run(request) + + assert credential.get_token.call_count == 1 + + tenant = "tenant-id" + endpoint = "https://authority.net/{}".format(tenant) + + challenge = Mock( + status_code=401, + headers={"WWW-Authenticate": 'Bearer authorization="{}", resource=https://challenge.resource'.format(endpoint)}, + ) + + test_with_challenge(challenge, tenant) + + @empty_challenge_cache def test_policy_updates_cache(): """ @@ -242,11 +337,7 @@ def get_token(*_, **__): @empty_challenge_cache def test_preserves_options_and_headers(): - """After a challenge, the original request should be sent with its options and headers preserved. - - If a policy mutates the options or headers of the challenge (unauthorized) request, the options of the service - request should be present when it is sent with authorization. - """ + """After a challenge, the policy should send the original request with its options and headers preserved""" url = get_random_url() token = "**" @@ -265,51 +356,31 @@ def get_token(*_, **__): ] + [mock_response()] * 2, ) - challenge_policy = ChallengeAuthPolicy(credential=credential) - policies = get_policies_for_request_mutation_test(challenge_policy) - pipeline = Pipeline(policies=policies, transport=transport) - - response = pipeline.run(HttpRequest("GET", url)) - - # ensure the mock sans I/O policies were called - for policy in policies: - if hasattr(policy, "on_request"): - assert policy.on_request.called, "mock policy wasn't invoked" - -def get_policies_for_request_mutation_test(challenge_policy): - # create mock policies to add, remove, and verify an option and header key = "foo" value = "bar" - do_not_handle = lambda _: False def add(request): # add the expected option and header request.context.options[key] = value request.http_request.headers[key] = value - adder = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=add), on_exception=do_not_handle) - - def remove(request): - # remove expected header and all options of unauthorized (challenge) requests - if not request.http_request.headers.get("Authorization"): - request.http_request.headers.pop(key, None) - request.context.options = {} - - remover = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=remove), on_exception=do_not_handle) + adder = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=add), on_exception=lambda _: False) def verify(request): # authorized (non-challenge) requests should have the expected option and header if request.http_request.headers.get("Authorization"): - assert request.context.options.get(key) == value, "request option not preserved across challenge" - assert request.http_request.headers.get(key) == value, "headers not preserved across challenge" + assert request.context.options.get(key) == value, "request option wasn't preserved across challenge" + assert request.http_request.headers.get(key) == value, "headers wasn't preserved across challenge" verifier = Mock(spec=SansIOHTTPPolicy, on_request=Mock(wraps=verify)) - # Mutating the challenge request shouldn't affect the authorized request. - # This is the pipeline flow: - # 1. add option and header - # 2. challenge auth - # 3. remove option, header from unauthorized request - # 4. verify option, header on authorized request - return [adder, challenge_policy, remover, verifier] + challenge_policy = ChallengeAuthPolicy(credential=credential) + policies = [adder, challenge_policy, verifier] + pipeline = Pipeline(policies=policies, transport=transport) + + pipeline.run(HttpRequest("GET", url)) + + # ensure the mock sans I/O policies were called + assert adder.on_request.called, "mock policy wasn't invoked" + assert verifier.on_request.called, "mock policy wasn't invoked" diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py index cf00e192ffa2..0ef669809548 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py @@ -6,23 +6,66 @@ Tests for the HTTP challenge authentication implementation. These tests aren't parallelizable, because the challenge cache is global to the process. """ +import asyncio +import os import time +from uuid import uuid4 -try: - from unittest.mock import Mock, patch -except ImportError: # python < 3.3 - from mock import Mock, patch # type: ignore +from unittest.mock import Mock, patch from azure.core.credentials import AccessToken from azure.core.exceptions import ServiceRequestError from azure.core.pipeline import AsyncPipeline +from azure.core.pipeline.policies import SansIOHTTPPolicy from azure.core.pipeline.transport import HttpRequest +from azure.identity.aio import ClientSecretCredential +from azure.keyvault.keys.aio import KeyClient from azure.keyvault.keys._shared import AsyncChallengeAuthPolicy, HttpChallenge, HttpChallengeCache +from azure.keyvault.keys._shared.client_base import DEFAULT_VERSION import pytest from _shared.helpers import mock_response, Request from _shared.helpers_async import async_validating_transport -from test_challenge_auth import empty_challenge_cache, get_policies_for_request_mutation_test, get_random_url +from _shared.test_case_async import KeyVaultTestCase +from _test_case import client_setup, get_decorator, KeysTestCase +from test_challenge_auth import empty_challenge_cache, get_random_url + + +only_default_version = get_decorator(is_async=True, api_versions=[DEFAULT_VERSION]) + + +class ChallengeAuthTests(KeysTestCase, KeyVaultTestCase): + def __init__(self, *args, **kwargs): + super().__init__(*args, match_body=False, **kwargs) + + @only_default_version() + @client_setup + async def test_multitenant_authentication(self, client, is_hsm, **kwargs): + if not self.is_live: + pytest.skip("This test is incompatible with vcrpy in playback") + + client_id = os.environ.get("KEYVAULT_CLIENT_ID") + client_secret = os.environ.get("KEYVAULT_CLIENT_SECRET") + if not (client_id and client_secret): + pytest.skip("Values for KEYVAULT_CLIENT_ID and KEYVAULT_CLIENT_SECRET are required") + + # we set up a client for this method so it gets awaited, but we actually want to create a new client + # this new client should use a credential with an initially fake tenant ID and still succeed with a real request + credential = ClientSecretCredential(tenant_id=str(uuid4()), client_id=client_id, client_secret=client_secret) + vault_url = self.managed_hsm_url if is_hsm else self.vault_url + client = KeyClient(vault_url=vault_url, credential=credential) + + if self.is_live: + await asyncio.sleep(2) # to avoid throttling by the service + key_name = self.get_resource_name("multitenant-key") + key = await client.create_rsa_key(key_name) + assert key.id + + # try making another request with the credential's token revoked + # the challenge policy should correctly request a new token for the correct tenant when a challenge is cached + client._client._config.authentication_policy._token = None + fetched_key = await client.get_key(key_name) + assert key.id == fetched_key.id @pytest.mark.asyncio @@ -65,13 +108,15 @@ async def send(request): return Mock(status_code=200) raise ValueError("unexpected request") - async def get_token(*scopes): + async def get_token(*scopes, **_): assert len(scopes) == 1 assert scopes[0] == expected_scope return AccessToken(expected_token, 0) credential = Mock(get_token=Mock(wraps=get_token)) - pipeline = AsyncPipeline(policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send)) + pipeline = AsyncPipeline( + policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send) + ) request = HttpRequest("POST", get_random_url()) request.set_bytes_body(expected_content) await pipeline.run(request) @@ -97,6 +142,59 @@ async def get_token(*scopes): await test_with_challenge(challenge_with_scope, scope) +@pytest.mark.asyncio +@empty_challenge_cache +async def test_tenant(): + """The policy's token requests should pass the parsed tenant ID from the challenge""" + + expected_content = b"a duck" + + async def test_with_challenge(challenge, expected_tenant): + expected_token = "expected_token" + + class Requests: + count = 0 + + async def send(request): + Requests.count += 1 + if Requests.count == 1: + # first request should be unauthorized and have no content + assert not request.body + assert request.headers["Content-Length"] == "0" + return challenge + elif Requests.count == 2: + # second request should be authorized according to challenge and have the expected content + assert request.headers["Content-Length"] + assert request.body == expected_content + assert expected_token in request.headers["Authorization"] + return Mock(status_code=200) + raise ValueError("unexpected request") + + async def get_token(*_, **kwargs): + assert kwargs.get("tenant_id") == expected_tenant + return AccessToken(expected_token, 0) + + credential = Mock(get_token=Mock(wraps=get_token)) + pipeline = AsyncPipeline( + policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send) + ) + request = HttpRequest("POST", get_random_url()) + request.set_bytes_body(expected_content) + await pipeline.run(request) + + assert credential.get_token.call_count == 1 + + tenant = "tenant-id" + endpoint = "https://authority.net/{}".format(tenant) + + challenge = Mock( + status_code=401, + headers={"WWW-Authenticate": 'Bearer authorization="{}", resource=https://challenge.resource'.format(endpoint)}, + ) + + await test_with_challenge(challenge, tenant) + + @pytest.mark.asyncio @empty_challenge_cache async def test_policy_updates_cache(): @@ -204,11 +302,7 @@ async def get_token(*_, **__): @pytest.mark.asyncio @empty_challenge_cache async def test_preserves_options_and_headers(): - """After a challenge, the original request should be sent with its options and headers preserved. - - If a policy mutates the options or headers of the challenge (unauthorized) request, the options of the service - request should be present when it is sent with authorization. - """ + """After a challenge, the policy should send the original request with its options and headers preserved""" url = get_random_url() @@ -228,13 +322,30 @@ async def get_token(*_, **__): ] + [mock_response()] * 2, ) + key = "foo" + value = "bar" + + def add(request): + # add the expected option and header + request.context.options[key] = value + request.http_request.headers[key] = value + + adder = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=add), on_exception=lambda _: False) + + def verify(request): + # authorized (non-challenge) requests should have the expected option and header + if request.http_request.headers.get("Authorization"): + assert request.context.options.get(key) == value, "request option wasn't preserved across challenge" + assert request.http_request.headers.get(key) == value, "headers wasn't preserved across challenge" + + verifier = Mock(spec=SansIOHTTPPolicy, on_request=Mock(wraps=verify)) + challenge_policy = AsyncChallengeAuthPolicy(credential=credential) - policies = get_policies_for_request_mutation_test(challenge_policy) + policies = [adder, challenge_policy, verifier] pipeline = AsyncPipeline(policies=policies, transport=transport) - response = await pipeline.run(HttpRequest("GET", url)) + await pipeline.run(HttpRequest("GET", url)) - # ensure the mock sans I/O policies were used - for policy in policies: - if hasattr(policy, "on_request"): - assert policy.on_request.called, "mock policy wasn't invoked" + # ensure the mock sans I/O policies were called + assert adder.on_request.called, "mock policy wasn't invoked" + assert verifier.on_request.called, "mock policy wasn't invoked" diff --git a/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md b/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md index 679cdf8ae1ba..c7858f5e7165 100644 --- a/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md @@ -3,12 +3,15 @@ ## 4.4.0b2 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.4.0b1 (2021-09-09) diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py index 3329ce2df068..d8303b4b081d 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py @@ -9,7 +9,7 @@ import urlparse as parse # type: ignore from typing import TYPE_CHECKING -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -21,7 +21,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-secrets/setup.py b/sdk/keyvault/azure-keyvault-secrets/setup.py index b8e377b75496..d74810b67dbf 100644 --- a/sdk/keyvault/azure-keyvault-secrets/setup.py +++ b/sdk/keyvault/azure-keyvault-secrets/setup.py @@ -83,7 +83,7 @@ ] ), install_requires=[ - "azure-core<2.0.0,>=1.7.0", + "azure-core<2.0.0,>=1.15.0", "msrest>=0.6.21", "azure-common~=1.1", ], diff --git a/sdk/monitor/azure-monitor-query/CHANGELOG.md b/sdk/monitor/azure-monitor-query/CHANGELOG.md index eb45cefa2f2c..11df874fa23e 100644 --- a/sdk/monitor/azure-monitor-query/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-query/CHANGELOG.md @@ -1,17 +1,11 @@ # Release History -## 1.0.1 (Unreleased) - -### Features Added - -### Breaking Changes +## 1.0.1 (2021-11-09) ### Bugs Fixed - Fixed a bug where Metadata values in timestamp don't show up sometimes. -### Other Changes - ## 1.0.0 (2021-10-06) ### Features Added diff --git a/sdk/monitor/azure-monitor-query/README.md b/sdk/monitor/azure-monitor-query/README.md index e40bacc0b17f..aac6d1f59b7a 100644 --- a/sdk/monitor/azure-monitor-query/README.md +++ b/sdk/monitor/azure-monitor-query/README.md @@ -445,7 +445,7 @@ This project has adopted the [Microsoft Open Source Code of Conduct][code_of_con [package]: https://aka.ms/azsdk-python-monitor-query-pypi [pip]: https://pypi.org/project/pip/ [python_logging]: https://docs.python.org/3/library/logging.html -[python-query-ref-docs]: https://docs.microsoft.com/python/api/overview/azure/monitor-query-readme?view=azure-python-preview +[python-query-ref-docs]: https://aka.ms/azsdk/python/monitor-query/docs [samples]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-query/samples [source]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-query/ diff --git a/sdk/monitor/azure-monitor-query/migration_guide.md b/sdk/monitor/azure-monitor-query/migration_guide.md new file mode 100644 index 000000000000..631167991a76 --- /dev/null +++ b/sdk/monitor/azure-monitor-query/migration_guide.md @@ -0,0 +1,100 @@ +# Guide for migrating from azure-loganalytics v0.1.0 to azure-monitor-query v1.0.x + +This guide assists you in the migration from [azure-loganalytics](https://pypi.org/project/azure-loganalytics/) v0.1.0 to [azure-monitor-query](https://pypi.org/project/azure-monitor-query/) v1.0.x. Side-by-side comparisons are provided for similar operations between the two packages. + +Familiarity with the `azure-loganalytics` v0.1.0 package is assumed. If you're new to the Azure Monitor Query client library for Python, see the [README for `azure-monitor-query`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-query/README.md) instead of this guide. + +## Table of contents + +- [Migration benefits](#migration-benefits) + - [Cross-service SDK improvements](#cross-service-sdk-improvements) + - [New features](#new-features) +- [Important changes](#important-changes) + - [The client](#the-client) + - [Client constructors and authentication](#client-constructors-and-authentication) + - [Send a single query request](#sending-a-single-query-request) +- [Additional samples](#additional-samples) + +## Migration benefits + +A natural question to ask when considering whether to adopt a new version or library is what the benefits of doing so would be. As Azure has matured and been embraced by a more diverse group of developers, we've focused on learning the patterns and practices to best support developer productivity and to understand the gaps that the Python client libraries have. + +Several areas of consistent feedback were expressed across the Azure client library ecosystem. One of the most important is that the client libraries for different Azure services haven't had a consistent approach to organization, naming, and API structure. Additionally, many developers have felt that the learning curve was too steep. The APIs didn't offer an approachable and consistent onboarding story for those learning Azure or exploring a specific Azure service. + +To improve the development experience across Azure services, a set of uniform [design guidelines](https://azure.github.io/azure-sdk/general_introduction.html) was created for all languages to drive a consistent experience with established API patterns for all services. A set of [Python-specific guidelines](https://azure.github.io/azure-sdk/python/guidelines/index.html) was also introduced to ensure that Python clients have a natural and idiomatic feel with respect to the Python ecosystem. Further details are available in the guidelines. + +### Cross-service SDK improvements + +The Azure Monitor Query client library also takes advantage of the cross-service improvements made to the Azure development experience. Examples include: + +- Using the new [`azure-identity`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/identity/azure-identity/README.md) library to share a single authentication approach between clients. +- A unified logging and diagnostics pipeline offering a common view of the activities across each of the client libraries. + +### New features + +There are a variety of new features in version 1.0 of the Monitor Query library. Some include: + +- The ability to execute a batch of queries with the `LogsQueryClient.query_batch()` API. +- The ability to configure the retry policy used by the operations on the client. +- Authentication with Azure Active Directory (Azure AD) credentials using [`azure-identity`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/identity/azure-identity/README.md). + +For more new features, changes, and bug fixes, see the [change log](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-query/CHANGELOG.md). + +## Important changes + +### The client + +To provide a more intuitive experience, the top-level client to query logs was renamed to `LogsQueryClient` from `LogAnalyticsDataClient`. `LogsQueryClient` can be authenticated using Azure AD. This client is the single entry point to execute a single query or a batch of queries. + +#### Consistency + +There are now methods with similar names, signatures, and locations to create senders and receivers. The result is consistency and predictability on the various features of the library. + +### Client constructors and authentication + +In `azure-loganalytics` v0.1.0: + +```python +from azure.loganalytics import LogAnalyticsDataClient +from msrestazure.azure_active_directory import ServicePrincipalCredentials + +credential = ServicePrincipalCredentials(...) +client = LogAnalyticsDataClient(credentials=credential) +``` + +In `azure-monitor-query` v1.0.x: + +```python +from azure.monitor.query import LogsQueryClient +from azure.identity import DefaultAzureCredential + +credential = DefaultAzureCredential() +client = LogsQueryClient(credential=credential) +``` + +### Send a single query request + +In version 1.0 of the Monitor Query library: + +- The `QueryBody` is flattened. Users are expected to pass the Kusto query directly to the API. +- The `timespan` attribute is now required, which helped to avoid querying over the entire data set. + +In `azure-loganalytics` v0.1.0: + +```python +from azure.loganalytics.models import QueryBody + +query = 'AppRequests | take 5' +response = client.query(workspace_id, QueryBody(**{'query': query})) +``` + +In `azure-monitor-query` v1.0.x: + +```python +query = 'AppRequests | take 5' +client.query(workspace_id, query, timespan=timedelta(days=1)) +``` + +## Additional samples + +For more examples, see [Samples for azure-monitor-query](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-query/samples). diff --git a/sdk/schemaregistry/azure-schemaregistry/CHANGELOG.md b/sdk/schemaregistry/azure-schemaregistry/CHANGELOG.md index 03081b5ede1c..1959fdc48004 100644 --- a/sdk/schemaregistry/azure-schemaregistry/CHANGELOG.md +++ b/sdk/schemaregistry/azure-schemaregistry/CHANGELOG.md @@ -1,15 +1,15 @@ # Release History -## 1.0.0 (2021-11-09) +## 1.0.0 (2021-11-10) **Note:** This is the first stable release of our efforts to create a user-friendly and Pythonic client library for Azure Schema Registry. ### Features Added - `SchemaRegistryClient` is the top-level client class interacting with the Azure Schema Registry Service. It provides three methods: - - `register_schema`: Store schema in the service by providing schema group name, schema name, schema format and schema definition. + - `register_schema`: Store schema in the service by providing schema group name, schema name, schema definition, and schema format. - `get_schema`: Get schema definition and its properties by schema id. - - `get_schema_properties`: Get schema properties by providing schema group name, schema name, schema format and schema definition. + - `get_schema_properties`: Get schema properties by providing schema group name, schema name, schema definition, and schema format. - `SchemaProperties` has the following instance variables: `id` and `format`: - The type of `format` has been changed from `str` to `SchemaFormat`. - `Schema` has the following properties: `properties` and `definition`. @@ -21,11 +21,7 @@ - `version` instance variable in `SchemaProperties` has been removed. - `schema_definition` instance variable in `Schema` has been renamed `definition`. - `id` parameter in `get_schema` method on sync and async `SchemaRegistryClient` has been renamed `schema_id`. -- `name` parameter in `register_schema` and `get_schema_properties` methods on sync and async `SchemaRegistryClient` has been renamed `schema_name`. - -### Bugs Fixed - -### Other Changes +- `schema_definition` parameter in `register_schema` and `get_schema_properties` methods on sync and async `SchemaRegistryClient` has been renamed `definition`. ## 1.0.0b3 (2021-10-05) diff --git a/sdk/schemaregistry/azure-schemaregistry/README.md b/sdk/schemaregistry/azure-schemaregistry/README.md index bf0428fa92a7..c7cb2c921f04 100644 --- a/sdk/schemaregistry/azure-schemaregistry/README.md +++ b/sdk/schemaregistry/azure-schemaregistry/README.md @@ -86,9 +86,9 @@ from azure.schemaregistry import SchemaRegistryClient token_credential = DefaultAzureCredential() fully_qualified_namespace = os.environ['SCHEMA_REGISTRY_FULLY_QUALIFIED_NAMESPACE'] group_name = os.environ['SCHEMA_REGISTRY_GROUP'] -schema_name = "your-schema-name" +name = "your-schema-name" format = "Avro" -schema_definition = """ +definition = """ {"namespace": "example.avro", "type": "record", "name": "User", @@ -102,7 +102,7 @@ schema_definition = """ schema_registry_client = SchemaRegistryClient(fully_qualified_namespace=fully_qualified_namespace, credential=token_credential) with schema_registry_client: - schema_properties = schema_registry_client.register_schema(group_name, schema_name, schema_definition, format) + schema_properties = schema_registry_client.register_schema(group_name, name, definition, format) id = schema_properties.id ``` @@ -140,9 +140,9 @@ from azure.schemaregistry import SchemaRegistryClient token_credential = DefaultAzureCredential() fully_qualified_namespace = os.environ['SCHEMA_REGISTRY_FULLY_QUALIFIED_NAMESPACE'] group_name = os.environ['SCHEMA_REGISTRY_GROUP'] -schema_name = "your-schema-name" +name = "your-schema-name" format = "Avro" -schema_definition = """ +definition = """ {"namespace": "example.avro", "type": "record", "name": "User", @@ -156,7 +156,7 @@ schema_definition = """ schema_registry_client = SchemaRegistryClient(fully_qualified_namespace=fully_qualified_namespace, credential=token_credential) with schema_registry_client: - schema_properties = schema_registry_client.register_schema(group_name, schema_name, schema_definition, format) + schema_properties = schema_registry_client.register_schema(group_name, name, definition, format) id = schema_properties.id ``` @@ -196,7 +196,7 @@ schema_registry_client = SchemaRegistryClient("your_fully_qualified_namespace", Similarly, `logging_enable` can enable detailed logging for a single operation, even when it isn't enabled for the client: ```py -schema_registry_client.get_schema(id, logging_enable=True) +schema_registry_client.get_schema(schema_id, logging_enable=True) ``` ## Next steps diff --git a/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/_schema_registry_client.py b/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/_schema_registry_client.py index 73d7f09c6514..25737915d65c 100644 --- a/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/_schema_registry_client.py +++ b/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/_schema_registry_client.py @@ -49,7 +49,7 @@ class SchemaRegistryClient(object): :param credential: To authenticate managing the entities of the SchemaRegistry namespace. :type credential: ~azure.core.credentials.TokenCredential :keyword str api_version: The Schema Registry service API version to use for requests. - Default value and only accepted value currently is "2021-10". + Default value and only accepted value currently is "2021-10". .. admonition:: Example: @@ -91,8 +91,8 @@ def close(self): def register_schema( self, group_name, - schema_name, - schema_definition, + name, + definition, format, **kwargs # pylint:disable=redefined-builtin ): @@ -103,8 +103,8 @@ def register_schema( schema is created at latest version + 1. :param str group_name: Schema group under which schema should be registered. - :param str schema_name: Name of schema being registered. - :param str schema_definition: String representation of the schema being registered. + :param str name: Name of schema being registered. + :param str definition: String representation of the schema being registered. :param format: Format for the schema being registered. For now Avro is the only supported schema format by the service. :type format: Union[str, SchemaFormat] @@ -130,8 +130,8 @@ def register_schema( http_request_kwargs = get_http_request_kwargs(kwargs) request = schema_rest.build_register_request( group_name=group_name, - schema_name=schema_name, - content=schema_definition, + schema_name=name, + content=definition, content_type=kwargs.pop( "content_type", "application/json; serialization={}".format(format) ), @@ -173,8 +173,8 @@ def get_schema(self, schema_id, **kwargs): def get_schema_properties( self, group_name, - schema_name, - schema_definition, + name, + definition, format, **kwargs # pylint:disable=redefined-builtin ): @@ -184,8 +184,8 @@ def get_schema_properties( as matched by schema definition comparison. :param str group_name: Schema group under which schema should be registered. - :param str schema_name: Name of schema being registered. - :param str schema_definition: String representation of the schema being registered. + :param str name: Name of schema being registered. + :param str definition: String representation of the schema being registered. :param format: Format for the schema being registered. :type format: Union[str, SchemaFormat] :rtype: ~azure.schemaregistry.SchemaProperties @@ -210,8 +210,8 @@ def get_schema_properties( http_request_kwargs = get_http_request_kwargs(kwargs) request = schema_rest.build_query_id_by_content_request( group_name=group_name, - schema_name=schema_name, - content=schema_definition, + schema_name=name, + content=definition, content_type=kwargs.pop( "content_type", "application/json; serialization={}".format(format) ), diff --git a/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/aio/_schema_registry_client_async.py b/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/aio/_schema_registry_client_async.py index afcebf00aae6..840815a6844f 100644 --- a/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/aio/_schema_registry_client_async.py +++ b/sdk/schemaregistry/azure-schemaregistry/azure/schemaregistry/aio/_schema_registry_client_async.py @@ -49,7 +49,7 @@ class SchemaRegistryClient(object): :param credential: To authenticate managing the entities of the SchemaRegistry namespace. :type credential: ~azure.core.credentials_async.AsyncTokenCredential :keyword str api_version: The Schema Registry service API version to use for requests. - Default value and only accepted value currently is "2021-10". + Default value and only accepted value currently is "2021-10". .. admonition:: Example: @@ -92,8 +92,8 @@ async def close(self) -> None: async def register_schema( self, group_name: str, - schema_name: str, - schema_definition: str, + name: str, + definition: str, format: Union[str, SchemaFormat], # pylint:disable=redefined-builtin **kwargs: Any ) -> SchemaProperties: @@ -103,8 +103,8 @@ async def register_schema( schema is created at latest version + 1. :param str group_name: Schema group under which schema should be registered. - :param str schema_name: Name of schema being registered. - :param str schema_definition: String representation of the schema being registered. + :param str name: Name of schema being registered. + :param str definition: String representation of the schema being registered. :param format: Format for the schema being registered. For now Avro is the only supported schema format by the service. :type format: Union[str, ~azure.schemaregistry.SchemaFormat] @@ -130,8 +130,8 @@ async def register_schema( http_request_kwargs = get_http_request_kwargs(kwargs) request = schema_rest.build_register_request( group_name=group_name, - schema_name=schema_name, - content=schema_definition, + schema_name=name, + content=definition, content_type=kwargs.pop( "content_type", "application/json; serialization={}".format(format) ), @@ -172,8 +172,8 @@ async def get_schema(self, schema_id: str, **kwargs: Any) -> Schema: async def get_schema_properties( self, group_name: str, - schema_name: str, - schema_definition: str, + name: str, + definition: str, format: Union[str, SchemaFormat], # pylint:disable=redefined-builtin **kwargs: Any ) -> SchemaProperties: @@ -182,8 +182,8 @@ async def get_schema_properties( as matched by schema defintion comparison. :param str group_name: Schema group under which schema should be registered. - :param str schema_name: Name of schema being registered. - :param str schema_definition: String representation of the schema being registered. + :param str name: Name of schema being registered. + :param str definition: String representation of the schema being registered. :param format: Format for the schema being registered. :type format: Union[str, ~azure.schemaregistry.SchemaFormat] :rtype: ~azure.schemaregistry.SchemaProperties @@ -208,8 +208,8 @@ async def get_schema_properties( http_request_kwargs = get_http_request_kwargs(kwargs) request = schema_rest.build_query_id_by_content_request( group_name=group_name, - schema_name=schema_name, - content=schema_definition, + schema_name=name, + content=definition, content_type=kwargs.pop( "content_type", "application/json; serialization={}".format(format) ), diff --git a/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/sample_code_schemaregistry_async.py b/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/sample_code_schemaregistry_async.py index 19fc713b01fd..f5471ddda41a 100644 --- a/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/sample_code_schemaregistry_async.py +++ b/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/sample_code_schemaregistry_async.py @@ -61,7 +61,7 @@ def create_client(): async def register_schema(schema_registry_client): # [START register_schema_async] GROUP_NAME = os.environ["SCHEMAREGISTRY_GROUP"] - SCHEMA_NAME = "your-schema-name" + NAME = "your-schema-name" FORMAT = "Avro" SCHEMA_JSON = { "namespace": "example.avro", @@ -73,9 +73,9 @@ async def register_schema(schema_registry_client): {"name": "favorite_color", "type": ["string", "null"]}, ], } - SCHEMA_DEFINITION = json.dumps(SCHEMA_JSON, separators=(",", ":")) + DEFINITION = json.dumps(SCHEMA_JSON, separators=(",", ":")) schema_properties = await schema_registry_client.register_schema( - GROUP_NAME, SCHEMA_NAME, SCHEMA_DEFINITION, FORMAT + GROUP_NAME, NAME, DEFINITION, FORMAT ) schema_id = schema_properties.id # [END register_schema_async] @@ -96,7 +96,7 @@ async def get_schema(schema_registry_client, schema_id): async def get_schema_id(schema_registry_client): # [START get_schema_id_async] group_name = os.environ["SCHEMAREGISTRY_GROUP"] - schema_name = "your-schema-name" + name = "your-schema-name" format = "Avro" schema_json = { "namespace": "example.avro", @@ -108,9 +108,9 @@ async def get_schema_id(schema_registry_client): {"name": "favorite_color", "type": ["string", "null"]}, ], } - schema_definition = json.dumps(schema_json, separators=(",", ":")) + definition = json.dumps(schema_json, separators=(",", ":")) schema_properties = await schema_registry_client.get_schema_properties( - group_name, schema_name, schema_definition, format + group_name, name, definition, format ) schema_id = schema_properties.id # [END get_schema_id_async] diff --git a/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/schema_registry_async.py b/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/schema_registry_async.py index 0d1b07a2d8b4..eaa9a86e5f39 100644 --- a/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/schema_registry_async.py +++ b/sdk/schemaregistry/azure-schemaregistry/samples/async_samples/schema_registry_async.py @@ -31,7 +31,7 @@ SCHEMAREGISTRY_FQN = os.environ["SCHEMAREGISTRY_FULLY_QUALIFIED_NAMESPACE"] GROUP_NAME = os.environ["SCHEMAREGISTRY_GROUP"] -SCHEMA_NAME = "your-schema-name" +NAME = "your-schema-name" FORMAT = SchemaFormat.AVRO SCHEMA_JSON = { "namespace": "example.avro", @@ -43,13 +43,13 @@ {"name": "favorite_color", "type": ["string", "null"]}, ], } -SCHEMA_STRING = json.dumps(SCHEMA_JSON, separators=(",", ":")) +DEFINITION = json.dumps(SCHEMA_JSON, separators=(",", ":")) -async def register_schema(client, group_name, schema_name, schema_string, format): +async def register_schema(client, group_name, name, definition, format): print("Registering schema...") schema_properties = await client.register_schema( - group_name, schema_name, schema_string, format + group_name, name, definition, format ) print("Schema registered, returned schema id is {}".format(schema_properties.id)) print("Schema properties are {}".format(schema_properties)) @@ -68,10 +68,10 @@ async def get_schema_by_id(client, schema_id): return schema.definition -async def get_schema_id(client, group_name, schema_name, schema_string, format): +async def get_schema_id(client, group_name, name, definition, format): print("Getting schema id...") schema_properties = await client.get_schema_properties( - group_name, schema_name, schema_string, format + group_name, name, definition, format ) print("The schema id is: {}".format(schema_properties.id)) print("Schema properties are {}".format(schema_properties)) @@ -85,11 +85,11 @@ async def main(): ) async with token_credential, schema_registry_client: schema_id = await register_schema( - schema_registry_client, GROUP_NAME, SCHEMA_NAME, SCHEMA_STRING, FORMAT + schema_registry_client, GROUP_NAME, NAME, DEFINITION, FORMAT ) schema_str = await get_schema_by_id(schema_registry_client, schema_id) schema_id = await get_schema_id( - schema_registry_client, GROUP_NAME, SCHEMA_NAME, SCHEMA_STRING, FORMAT + schema_registry_client, GROUP_NAME, NAME, DEFINITION, FORMAT ) diff --git a/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/sample_code_schemaregistry.py b/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/sample_code_schemaregistry.py index 5aaa7c5621ad..0ad140718454 100644 --- a/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/sample_code_schemaregistry.py +++ b/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/sample_code_schemaregistry.py @@ -60,7 +60,7 @@ def create_client(): def register_schema(schema_registry_client): # [START register_schema_sync] GROUP_NAME = os.environ["SCHEMAREGISTRY_GROUP"] - SCHEMA_NAME = "your-schema-name" + NAME = "your-schema-name" FORMAT = "Avro" SCHEMA_JSON = { "namespace": "example.avro", @@ -72,9 +72,9 @@ def register_schema(schema_registry_client): {"name": "favorite_color", "type": ["string", "null"]}, ], } - SCHEMA_DEFINITION = json.dumps(SCHEMA_JSON, separators=(",", ":")) + DEFINTION = json.dumps(SCHEMA_JSON, separators=(",", ":")) schema_properties = schema_registry_client.register_schema( - GROUP_NAME, SCHEMA_NAME, SCHEMA_DEFINITION, FORMAT + GROUP_NAME, NAME, DEFINTION, FORMAT ) schema_id = schema_properties.id # [END register_schema_sync] @@ -95,7 +95,7 @@ def get_schema(schema_registry_client, schema_id): def get_schema_id(schema_registry_client): # [START get_schema_id_sync] group_name = os.environ["SCHEMAREGISTRY_GROUP"] - schema_name = "your-schema-name" + name = "your-schema-name" format = "Avro" schema_json = { "namespace": "example.avro", @@ -107,9 +107,9 @@ def get_schema_id(schema_registry_client): {"name": "favorite_color", "type": ["string", "null"]}, ], } - schema_definition = json.dumps(schema_json, separators=(",", ":")) + definition = json.dumps(schema_json, separators=(",", ":")) schema_properties = schema_registry_client.get_schema_properties( - group_name, schema_name, schema_definition, format + group_name, name, definition, format ) schema_id = schema_properties.id # [END get_schema_id_sync] diff --git a/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/schema_registry.py b/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/schema_registry.py index b9b8063b88e7..9c143a85063d 100644 --- a/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/schema_registry.py +++ b/sdk/schemaregistry/azure-schemaregistry/samples/sync_samples/schema_registry.py @@ -50,7 +50,7 @@ SCHEMAREGISTRY_FQN = os.environ["SCHEMAREGISTRY_FULLY_QUALIFIED_NAMESPACE"] GROUP_NAME = os.environ["SCHEMAREGISTRY_GROUP"] -SCHEMA_NAME = "your-schema-name" +NAME = "your-schema-name" FORMAT = SchemaFormat.AVRO SCHEMA_JSON = { @@ -63,13 +63,13 @@ {"name": "favorite_color", "type": ["string", "null"]}, ], } -SCHEMA_STRING = json.dumps(SCHEMA_JSON, separators=(",", ":")) +DEFINITION = json.dumps(SCHEMA_JSON, separators=(",", ":")) -def register_schema(client, group_name, schema_name, schema_string, format): +def register_schema(client, group_name, name, definition, format): print("Registering schema...") schema_properties = client.register_schema( - group_name, schema_name, schema_string, format + group_name, name, definition, format ) print("Schema registered, returned schema id is {}".format(schema_properties.id)) print("Schema properties are {}".format(schema_properties)) @@ -86,10 +86,10 @@ def get_schema_by_id(client, schema_id): return schema.definition -def get_schema_id(client, group_name, schema_name, schema_string, format): +def get_schema_id(client, group_name, name, definition, format): print("Getting schema id...") schema_properties = client.get_schema_properties( - group_name, schema_name, schema_string, format + group_name, name, definition, format ) print("The schema id is: {}".format(schema_properties.id)) print("Schema properties are {}".format(schema_properties)) @@ -103,9 +103,9 @@ def get_schema_id(client, group_name, schema_name, schema_string, format): ) with schema_registry_client: schema_id = register_schema( - schema_registry_client, GROUP_NAME, SCHEMA_NAME, SCHEMA_STRING, FORMAT + schema_registry_client, GROUP_NAME, NAME, DEFINITION, FORMAT ) schema_str = get_schema_by_id(schema_registry_client, schema_id) schema_id = get_schema_id( - schema_registry_client, GROUP_NAME, SCHEMA_NAME, SCHEMA_STRING, FORMAT + schema_registry_client, GROUP_NAME, NAME, DEFINITION, FORMAT ) diff --git a/sdk/schemaregistry/azure-schemaregistry/tests/async_tests/test_schema_registry_async.py b/sdk/schemaregistry/azure-schemaregistry/tests/async_tests/test_schema_registry_async.py index bce8e334ef6b..1932949f70e8 100644 --- a/sdk/schemaregistry/azure-schemaregistry/tests/async_tests/test_schema_registry_async.py +++ b/sdk/schemaregistry/azure-schemaregistry/tests/async_tests/test_schema_registry_async.py @@ -44,10 +44,10 @@ def create_client(self, fully_qualified_namespace): async def test_schema_basic_async(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client(schemaregistry_fully_qualified_namespace) async with client: - schema_name = self.get_resource_name('test-schema-basic-async') + name = self.get_resource_name('test-schema-basic-async') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" - schema_properties = await client.register_schema(schemaregistry_group, schema_name, schema_str, format, logging_enable=True) + schema_properties = await client.register_schema(schemaregistry_group, name, schema_str, format, logging_enable=True) assert schema_properties.id is not None assert schema_properties.format == "Avro" @@ -58,7 +58,7 @@ async def test_schema_basic_async(self, schemaregistry_fully_qualified_namespace assert returned_schema.properties.format == "Avro" assert returned_schema.definition == schema_str - returned_schema_properties = await client.get_schema_properties(schemaregistry_group, schema_name, schema_str, format, logging_enable=True) + returned_schema_properties = await client.get_schema_properties(schemaregistry_group, name, schema_str, format, logging_enable=True) assert returned_schema_properties.id == schema_properties.id assert returned_schema_properties.format == "Avro" @@ -68,16 +68,16 @@ async def test_schema_basic_async(self, schemaregistry_fully_qualified_namespace async def test_schema_update_async(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client(schemaregistry_fully_qualified_namespace) async with client: - schema_name = self.get_resource_name('test-schema-update-async') + name = self.get_resource_name('test-schema-update-async') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" - schema_properties = await client.register_schema(schemaregistry_group, schema_name, schema_str, format) + schema_properties = await client.register_schema(schemaregistry_group, name, schema_str, format) assert schema_properties.id is not None assert schema_properties.format == "Avro" schema_str_new = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_food","type":["string","null"]}]}""" - new_schema_properties = await client.register_schema(schemaregistry_group, schema_name, schema_str_new, format) + new_schema_properties = await client.register_schema(schemaregistry_group, name, schema_str_new, format) assert new_schema_properties.id is not None assert new_schema_properties.format == "Avro" @@ -94,12 +94,12 @@ async def test_schema_update_async(self, schemaregistry_fully_qualified_namespac @SchemaRegistryPowerShellPreparer() async def test_schema_same_twice_async(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client(schemaregistry_fully_qualified_namespace) - schema_name = self.get_resource_name('test-schema-twice-async') + name = self.get_resource_name('test-schema-twice-async') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"age","type":["int","null"]},{"name":"city","type":["string","null"]}]}""" format = "Avro" async with client: - schema_properties = await client.register_schema(schemaregistry_group, schema_name, schema_str, format) - schema_properties_second = await client.register_schema(schemaregistry_group, schema_name, schema_str, format) + schema_properties = await client.register_schema(schemaregistry_group, name, schema_str, format) + schema_properties_second = await client.register_schema(schemaregistry_group, name, schema_str, format) assert schema_properties.id == schema_properties_second.id await client._generated_client._config.credential.close() @@ -108,21 +108,21 @@ async def test_schema_negative_wrong_credential_async(self, schemaregistry_fully credential = ClientSecretCredential(tenant_id="fake", client_id="fake", client_secret="fake") client = SchemaRegistryClient(fully_qualified_namespace=schemaregistry_fully_qualified_namespace, credential=credential) async with client, credential: - schema_name = self.get_resource_name('test-schema-negative-async') + name = self.get_resource_name('test-schema-negative-async') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" with pytest.raises(ClientAuthenticationError): - await client.register_schema(schemaregistry_group, schema_name, schema_str, format) + await client.register_schema(schemaregistry_group, name, schema_str, format) @SchemaRegistryPowerShellPreparer() async def test_schema_negative_wrong_endpoint_async(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client("nonexist.servicebus.windows.net") async with client: - schema_name = self.get_resource_name('test-schema-nonexist-async') + name = self.get_resource_name('test-schema-nonexist-async') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" with pytest.raises(ServiceRequestError): - await client.register_schema(schemaregistry_group, schema_name, schema_str, format) + await client.register_schema(schemaregistry_group, name, schema_str, format) await client._generated_client._config.credential.close() @SchemaRegistryPowerShellPreparer() diff --git a/sdk/schemaregistry/azure-schemaregistry/tests/test_schema_registry.py b/sdk/schemaregistry/azure-schemaregistry/tests/test_schema_registry.py index dde92bae279a..7fd2f1a02da5 100644 --- a/sdk/schemaregistry/azure-schemaregistry/tests/test_schema_registry.py +++ b/sdk/schemaregistry/azure-schemaregistry/tests/test_schema_registry.py @@ -39,10 +39,10 @@ def create_client(self, fully_qualified_namespace): @SchemaRegistryPowerShellPreparer() def test_schema_basic(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client(schemaregistry_fully_qualified_namespace) - schema_name = self.get_resource_name('test-schema-basic') + name = self.get_resource_name('test-schema-basic') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" - schema_properties = client.register_schema(schemaregistry_group, schema_name, schema_str, format, logging_enable=True) + schema_properties = client.register_schema(schemaregistry_group, name, schema_str, format, logging_enable=True) assert schema_properties.id is not None assert schema_properties.format == "Avro" @@ -53,7 +53,7 @@ def test_schema_basic(self, schemaregistry_fully_qualified_namespace, schemaregi assert returned_schema.properties.format == "Avro" assert returned_schema.definition == schema_str - returned_schema_properties = client.get_schema_properties(schemaregistry_group, schema_name, schema_str, format, logging_enable=True) + returned_schema_properties = client.get_schema_properties(schemaregistry_group, name, schema_str, format, logging_enable=True) assert returned_schema_properties.id == schema_properties.id assert returned_schema_properties.format == "Avro" @@ -61,16 +61,16 @@ def test_schema_basic(self, schemaregistry_fully_qualified_namespace, schemaregi @SchemaRegistryPowerShellPreparer() def test_schema_update(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client(schemaregistry_fully_qualified_namespace) - schema_name = self.get_resource_name('test-schema-update') + name = self.get_resource_name('test-schema-update') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" - schema_properties = client.register_schema(schemaregistry_group, schema_name, schema_str, format) + schema_properties = client.register_schema(schemaregistry_group, name, schema_str, format) assert schema_properties.id is not None assert schema_properties.format == "Avro" schema_str_new = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_food","type":["string","null"]}]}""" - new_schema_properties = client.register_schema(schemaregistry_group, schema_name, schema_str_new, format) + new_schema_properties = client.register_schema(schemaregistry_group, name, schema_str_new, format) assert new_schema_properties.id is not None assert new_schema_properties.format == "Avro" @@ -85,31 +85,31 @@ def test_schema_update(self, schemaregistry_fully_qualified_namespace, schemareg @SchemaRegistryPowerShellPreparer() def test_schema_same_twice(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client(schemaregistry_fully_qualified_namespace) - schema_name = self.get_resource_name('test-schema-twice') + name = self.get_resource_name('test-schema-twice') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"age","type":["int","null"]},{"name":"city","type":["string","null"]}]}""" format = "Avro" - schema_properties = client.register_schema(schemaregistry_group, schema_name, schema_str, format) - schema_properties_second = client.register_schema(schemaregistry_group, schema_name, schema_str, format) + schema_properties = client.register_schema(schemaregistry_group, name, schema_str, format) + schema_properties_second = client.register_schema(schemaregistry_group, name, schema_str, format) assert schema_properties.id == schema_properties_second.id @SchemaRegistryPowerShellPreparer() def test_schema_negative_wrong_credential(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): credential = ClientSecretCredential(tenant_id="fake", client_id="fake", client_secret="fake") client = SchemaRegistryClient(fully_qualified_namespace=schemaregistry_fully_qualified_namespace, credential=credential) - schema_name = self.get_resource_name('test-schema-negative') + name = self.get_resource_name('test-schema-negative') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" with pytest.raises(ClientAuthenticationError): - client.register_schema(schemaregistry_group, schema_name, schema_str, format) + client.register_schema(schemaregistry_group, name, schema_str, format) @SchemaRegistryPowerShellPreparer() def test_schema_negative_wrong_endpoint(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): client = self.create_client("nonexist.servicebus.windows.net") - schema_name = self.get_resource_name('test-schema-nonexist') + name = self.get_resource_name('test-schema-nonexist') schema_str = """{"namespace":"example.avro","type":"record","name":"User","fields":[{"name":"name","type":"string"},{"name":"favorite_number","type":["int","null"]},{"name":"favorite_color","type":["string","null"]}]}""" format = "Avro" with pytest.raises(ServiceRequestError): - client.register_schema(schemaregistry_group, schema_name, schema_str, format) + client.register_schema(schemaregistry_group, name, schema_str, format) @SchemaRegistryPowerShellPreparer() def test_schema_negative_no_schema(self, schemaregistry_fully_qualified_namespace, schemaregistry_group, **kwargs): diff --git a/sdk/storage/azure-storage-blob/CHANGELOG.md b/sdk/storage/azure-storage-blob/CHANGELOG.md index 926e10f8cb56..4f45c5a331e5 100644 --- a/sdk/storage/azure-storage-blob/CHANGELOG.md +++ b/sdk/storage/azure-storage-blob/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 12.10.0b2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 12.10.0b1 (2021-11-08) **New Features** - Account level SAS tokens now support two new permissions: @@ -430,4 +440,4 @@ https://aka.ms/azure-sdk-preview1-python. - Enabling MD5 validation no longer uses the memory-efficient algorithm for large block blobs, since computing the MD5 hash requires reading the entire block into memory. - Fixed a bug in the _SubStream class which was at risk of causing data corruption when using the memory-efficient algorithm for large block blobs. -- Support for AccessTierChangeTime to get the last time a tier was modified on an individual blob. \ No newline at end of file +- Support for AccessTierChangeTime to get the last time a tier was modified on an individual blob. diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py index c5b76ddced4d..c7e10507372c 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_version.py @@ -4,4 +4,4 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "12.10.0b1" +VERSION = "12.10.0b2" diff --git a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md index 37bdde2c688c..f27bd5de0214 100644 --- a/sdk/storage/azure-storage-file-datalake/CHANGELOG.md +++ b/sdk/storage/azure-storage-file-datalake/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 12.6.0b2 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 12.6.0b1 (2021-11-08) **New features** - Added support for batch deleting files using the `delete_files()` method from a `FileSystemClient` @@ -163,4 +173,4 @@ This package's [documentation](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-file-datalake/README.md) and -[samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-file-datalake/samples) \ No newline at end of file +[samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/storage/azure-storage-file-datalake/samples) diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py index 202620c82c6d..fa1f7b91264e 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_version.py @@ -4,4 +4,4 @@ # license information. # -------------------------------------------------------------------------- -VERSION = "12.6.0b1" +VERSION = "12.6.0b2" diff --git a/sdk/videoanalyzer/azure-media-videoanalyzer-edge/CHANGELOG.md b/sdk/videoanalyzer/azure-media-videoanalyzer-edge/CHANGELOG.md index e1f30a5abc40..5974d1df8356 100644 --- a/sdk/videoanalyzer/azure-media-videoanalyzer-edge/CHANGELOG.md +++ b/sdk/videoanalyzer/azure-media-videoanalyzer-edge/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 1.0.0b4 (Unreleased) + +### Features Added + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + ## 1.0.0b3 (2021-11-09) - Added device discovery and device detail request for ONVIF enabled devices. diff --git a/sdk/videoanalyzer/azure-media-videoanalyzer-edge/azure/media/videoanalyzeredge/_version.py b/sdk/videoanalyzer/azure-media-videoanalyzer-edge/azure/media/videoanalyzeredge/_version.py index 9c0a682a433e..5684f6e3fbce 100644 --- a/sdk/videoanalyzer/azure-media-videoanalyzer-edge/azure/media/videoanalyzeredge/_version.py +++ b/sdk/videoanalyzer/azure-media-videoanalyzer-edge/azure/media/videoanalyzeredge/_version.py @@ -4,4 +4,4 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------- -VERSION = "1.0.0b3" +VERSION = "1.0.0b4" diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/CHANGELOG.md b/sdk/webpubsub/azure-messaging-webpubsubservice/CHANGELOG.md index 43b0a94039fc..ed59df48d706 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/CHANGELOG.md +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/CHANGELOG.md @@ -10,6 +10,7 @@ - remove the `operations` namespace from `azure.messaging.webpubsubservice` - rename operation `check_permission` to `has_permission` - operations `connection_exists`, `group_exists`, `user_exists`, and `has_permission` now return boolean values instead of raising +- move parameter 'hub' from operation to client ### Bug Fixes diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/README.md b/sdk/webpubsub/azure-messaging-webpubsubservice/README.md index 9694d7259937..a8f01f9017a0 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/README.md +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/README.md @@ -36,7 +36,7 @@ You can authenticate the `WebPubSubServiceClient` using [connection string][conn ```python >>> from azure.messaging.webpubsubservice import WebPubSubServiceClient ->>> service = WebPubSubServiceClient.from_connection_string(connection_string='') +>>> service = WebPubSubServiceClient.from_connection_string(connection_string='', hub='hub') ``` Or using the service endpoint and the access key: @@ -45,7 +45,7 @@ Or using the service endpoint and the access key: >>> from azure.messaging.webpubsubservice import WebPubSubServiceClient >>> from azure.core.credentials import AzureKeyCredential ->>> service = WebPubSubServiceClient(endpoint='', credential=AzureKeyCredential("")) +>>> service = WebPubSubServiceClient(endpoint='', hub='hub', credential=AzureKeyCredential("")) ``` Or using [Azure Active Directory][aad_doc]: @@ -56,7 +56,7 @@ Or using [Azure Active Directory][aad_doc]: ```python >>> from azure.messaging.webpubsubservice import WebPubSubServiceClient >>> from azure.identity import DefaultAzureCredential - >>> service = WebPubSubServiceClient(endpoint='', credential=DefaultAzureCredential()) + >>> service = WebPubSubServiceClient(endpoint='', hub='hub', credential=DefaultAzureCredential()) ``` ## Key concepts @@ -83,21 +83,39 @@ When the client is connected, it can send messages to the upstream application, ## Examples -### Broadcast messages +### Broadcast messages in JSON format ```python >>> from azure.messaging.webpubsubservice import WebPubSubServiceClient ->>> from azure.identity import DefaultAzureCredential ->>> from azure.core.exceptions import HttpResponseError ->>> service = WebPubSubServiceClient(endpoint='', credential=DefaultAzureCredential()) ->>> with open('file.json', 'r') as f: - try: - service.send_to_all('ahub', content=f) - except HttpResponseError as e: - print('service responds error: {}'.format(e.response.json())) +>>> service = WebPubSubServiceClient.from_connection_string('', hub='hub1') +>>> service.send_to_all(message = { + 'from': 'user1', + 'data': 'Hello world' + }) +``` + +The WebSocket client will receive JSON serialized text: `{"from": "user1", "data": "Hello world"}`. + +### Broadcast messages in plain-text format +```python +>>> from azure.messaging.webpubsubservice import WebPubSubServiceClient +>>> service = WebPubSubServiceClient.from_connection_string('', hub='hub1') +>>> service.send_to_all(message = 'Hello world', content_type='text/plain') +``` + +The WebSocket client will receive text: `Hello world`. + +### Broadcast messages in binary format + +```python +>>> import io +>>> from azure.messaging.webpubsubservice import WebPubSubServiceClient +>>> service = WebPubSubServiceClient.from_connection_string('', hub='hub') +>>> service.send_to_all(message=io.StringIO('Hello World'), content_type='application/octet-stream') ``` +The WebSocket client will receive binary text: `b'Hello world'`. ## Troubleshooting @@ -124,7 +142,7 @@ endpoint = "" credential = DefaultAzureCredential() # This WebPubSubServiceClient will log detailed information about its HTTP sessions, at DEBUG level -service = WebPubSubServiceClient(endpoint=endpoint, credential=credential, logging_enable=True) +service = WebPubSubServiceClient(endpoint=endpoint, hub='hub', credential=credential, logging_enable=True) ``` Similarly, `logging_enable` can enable detailed logging for a single call, @@ -138,7 +156,7 @@ Http request and response details are printed to stdout with this logging config ## Next steps -Check [more samples here][awps_samples]. +Check [more samples here][samples]. ## Contributing @@ -178,4 +196,4 @@ additional questions or comments. [azure_portal]: https://docs.microsoft.com/azure/azure-web-pubsub/howto-develop-create-instance [azure-key-credential]: https://aka.ms/azsdk-python-core-azurekeycredential [aad_doc]: https://aka.ms/awps/aad -[awps_samples]: https://github.com/Azure/azure-webpubsub/tree/main/samples/python +[samples]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/webpubsub/azure-messaging-webpubsubservice/samples diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_configuration.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_configuration.py index 44defc058a54..0ad85a86eb78 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_configuration.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_configuration.py @@ -26,6 +26,8 @@ class WebPubSubServiceClientConfiguration(Configuration): Note that all parameters used to create this instance are saved as instance attributes. + :param hub: Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore. + :type hub: str :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str :param credential: Credential needed for the client to connect to Azure. @@ -36,6 +38,7 @@ class WebPubSubServiceClientConfiguration(Configuration): def __init__( self, + hub, # type: str endpoint, # type: str credential, # type: "TokenCredential" **kwargs # type: Any @@ -44,11 +47,14 @@ def __init__( super(WebPubSubServiceClientConfiguration, self).__init__(**kwargs) api_version = kwargs.pop('api_version', "2021-10-01") # type: str + if hub is None: + raise ValueError("Parameter 'hub' must not be None.") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") if credential is None: raise ValueError("Parameter 'credential' must not be None.") + self.hub = hub self.endpoint = endpoint self.credential = credential self.api_version = api_version diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_operations/_operations.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_operations/_operations.py index 1eb379f1551b..a3e45932a41a 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_operations/_operations.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_operations/_operations.py @@ -26,6 +26,7 @@ ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] _SERIALIZER = Serializer() +_SERIALIZER.client_side_validation = False # fmt: off def build_get_client_access_token_request( @@ -798,7 +799,6 @@ class WebPubSubServiceClientOperationsMixin(object): @distributed_trace def get_client_access_token( self, - hub, # type: str **kwargs # type: Any ): # type: (...) -> JSONType @@ -806,9 +806,6 @@ def get_client_access_token( Generate token for the client to connect Azure Web PubSub service. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :keyword user_id: User Id. :paramtype user_id: str :keyword roles: Roles that the connection with the generated token will have. @@ -843,7 +840,7 @@ def get_client_access_token( request = build_get_client_access_token_request( - hub=hub, + hub=self._config.hub, api_version=api_version, user_id=user_id, roles=roles, @@ -878,7 +875,6 @@ def get_client_access_token( @distributed_trace def close_all_connections( self, - hub, # type: str **kwargs # type: Any ): # type: (...) -> None @@ -886,9 +882,6 @@ def close_all_connections( Close the connections in the hub. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :keyword excluded: Exclude these connectionIds when closing the connections in the hub. :paramtype excluded: list[str] :keyword reason: The reason closing the client connection. @@ -912,7 +905,7 @@ def close_all_connections( request = build_close_all_connections_request( - hub=hub, + hub=self._config.hub, api_version=api_version, excluded=excluded, reason=reason, @@ -939,7 +932,6 @@ def close_all_connections( @distributed_trace def send_to_all( self, - hub, # type: str message, # type: Union[IO, str, JSONType] **kwargs # type: Any ): @@ -948,9 +940,6 @@ def send_to_all( Broadcast content inside request body to all the connected client connections. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param message: The payload body. :type message: IO or str or JSONType :keyword excluded: Excluded connection Ids. @@ -988,7 +977,7 @@ def send_to_all( ) request = build_send_to_all_request( - hub=hub, + hub=self._config.hub, api_version=api_version, content_type=content_type, json=json, @@ -1017,7 +1006,6 @@ def send_to_all( @distributed_trace def connection_exists( self, - hub, # type: str connection_id, # type: str **kwargs # type: Any ): @@ -1026,9 +1014,6 @@ def connection_exists( Check if the connection with the given connectionId exists. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param connection_id: The connection Id. :type connection_id: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -1048,7 +1033,7 @@ def connection_exists( request = build_connection_exists_request( - hub=hub, + hub=self._config.hub, connection_id=connection_id, api_version=api_version, template_url=self.connection_exists.metadata['url'], @@ -1075,7 +1060,6 @@ def connection_exists( @distributed_trace def close_connection( self, - hub, # type: str connection_id, # type: str **kwargs # type: Any ): @@ -1084,9 +1068,6 @@ def close_connection( Close the client connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param connection_id: Target connection Id. :type connection_id: str :keyword reason: The reason closing the client connection. @@ -1109,7 +1090,7 @@ def close_connection( request = build_close_connection_request( - hub=hub, + hub=self._config.hub, connection_id=connection_id, api_version=api_version, reason=reason, @@ -1136,7 +1117,6 @@ def close_connection( @distributed_trace def send_to_connection( self, - hub, # type: str connection_id, # type: str message, # type: Union[IO, str, JSONType] **kwargs # type: Any @@ -1146,9 +1126,6 @@ def send_to_connection( Send content inside request body to the specific connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param connection_id: The connection Id. :type connection_id: str :param message: The payload body. @@ -1185,7 +1162,7 @@ def send_to_connection( ) request = build_send_to_connection_request( - hub=hub, + hub=self._config.hub, connection_id=connection_id, api_version=api_version, content_type=content_type, @@ -1214,7 +1191,6 @@ def send_to_connection( @distributed_trace def group_exists( self, - hub, # type: str group, # type: str **kwargs # type: Any ): @@ -1223,9 +1199,6 @@ def group_exists( Check if there are any client connections inside the given group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -1245,7 +1218,7 @@ def group_exists( request = build_group_exists_request( - hub=hub, + hub=self._config.hub, group=group, api_version=api_version, template_url=self.group_exists.metadata['url'], @@ -1272,7 +1245,6 @@ def group_exists( @distributed_trace def close_group_connections( self, - hub, # type: str group, # type: str **kwargs # type: Any ): @@ -1281,9 +1253,6 @@ def close_group_connections( Close connections in the specific group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :keyword excluded: Exclude these connectionIds when closing the connections in the group. @@ -1309,7 +1278,7 @@ def close_group_connections( request = build_close_group_connections_request( - hub=hub, + hub=self._config.hub, group=group, api_version=api_version, excluded=excluded, @@ -1337,7 +1306,6 @@ def close_group_connections( @distributed_trace def send_to_group( self, - hub, # type: str group, # type: str message, # type: Union[IO, str, JSONType] **kwargs # type: Any @@ -1347,9 +1315,6 @@ def send_to_group( Send content inside request body to a group of connections. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param message: The payload body. @@ -1389,7 +1354,7 @@ def send_to_group( ) request = build_send_to_group_request( - hub=hub, + hub=self._config.hub, group=group, api_version=api_version, content_type=content_type, @@ -1419,7 +1384,6 @@ def send_to_group( @distributed_trace def add_connection_to_group( self, - hub, # type: str group, # type: str connection_id, # type: str **kwargs # type: Any @@ -1429,9 +1393,6 @@ def add_connection_to_group( Add a connection to the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param connection_id: Target connection Id. @@ -1453,7 +1414,7 @@ def add_connection_to_group( request = build_add_connection_to_group_request( - hub=hub, + hub=self._config.hub, group=group, connection_id=connection_id, api_version=api_version, @@ -1480,7 +1441,6 @@ def add_connection_to_group( @distributed_trace def remove_connection_from_group( self, - hub, # type: str group, # type: str connection_id, # type: str **kwargs # type: Any @@ -1490,9 +1450,6 @@ def remove_connection_from_group( Remove a connection from the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param connection_id: Target connection Id. @@ -1514,7 +1471,7 @@ def remove_connection_from_group( request = build_remove_connection_from_group_request( - hub=hub, + hub=self._config.hub, group=group, connection_id=connection_id, api_version=api_version, @@ -1541,7 +1498,6 @@ def remove_connection_from_group( @distributed_trace def user_exists( self, - hub, # type: str user_id, # type: str **kwargs # type: Any ): @@ -1550,9 +1506,6 @@ def user_exists( Check if there are any client connections connected for the given user. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: Target user Id. :type user_id: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -1572,7 +1525,7 @@ def user_exists( request = build_user_exists_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, template_url=self.user_exists.metadata['url'], @@ -1599,7 +1552,6 @@ def user_exists( @distributed_trace def close_user_connections( self, - hub, # type: str user_id, # type: str **kwargs # type: Any ): @@ -1608,9 +1560,6 @@ def close_user_connections( Close connections for the specific user. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: The user Id. :type user_id: str :keyword excluded: Exclude these connectionIds when closing the connections for the user. @@ -1636,7 +1585,7 @@ def close_user_connections( request = build_close_user_connections_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, excluded=excluded, @@ -1664,7 +1613,6 @@ def close_user_connections( @distributed_trace def send_to_user( self, - hub, # type: str user_id, # type: str message, # type: Union[IO, str, JSONType] **kwargs # type: Any @@ -1674,9 +1622,6 @@ def send_to_user( Send content inside request body to the specific user. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: The user Id. :type user_id: str :param message: The payload body. @@ -1713,7 +1658,7 @@ def send_to_user( ) request = build_send_to_user_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, content_type=content_type, @@ -1742,7 +1687,6 @@ def send_to_user( @distributed_trace def add_user_to_group( self, - hub, # type: str group, # type: str user_id, # type: str **kwargs # type: Any @@ -1752,9 +1696,6 @@ def add_user_to_group( Add a user to the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param user_id: Target user Id. @@ -1776,7 +1717,7 @@ def add_user_to_group( request = build_add_user_to_group_request( - hub=hub, + hub=self._config.hub, group=group, user_id=user_id, api_version=api_version, @@ -1803,7 +1744,6 @@ def add_user_to_group( @distributed_trace def remove_user_from_group( self, - hub, # type: str group, # type: str user_id, # type: str **kwargs # type: Any @@ -1813,9 +1753,6 @@ def remove_user_from_group( Remove a user from the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param user_id: Target user Id. @@ -1837,7 +1774,7 @@ def remove_user_from_group( request = build_remove_user_from_group_request( - hub=hub, + hub=self._config.hub, group=group, user_id=user_id, api_version=api_version, @@ -1864,7 +1801,6 @@ def remove_user_from_group( @distributed_trace def remove_user_from_all_groups( self, - hub, # type: str user_id, # type: str **kwargs # type: Any ): @@ -1873,9 +1809,6 @@ def remove_user_from_all_groups( Remove a user from all groups. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: Target user Id. :type user_id: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -1895,7 +1828,7 @@ def remove_user_from_all_groups( request = build_remove_user_from_all_groups_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, template_url=self.remove_user_from_all_groups.metadata['url'], @@ -1921,7 +1854,6 @@ def remove_user_from_all_groups( @distributed_trace def grant_permission( self, - hub, # type: str permission, # type: str connection_id, # type: str **kwargs # type: Any @@ -1931,9 +1863,6 @@ def grant_permission( Grant permission to the connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param permission: The permission: current supported actions are joinLeaveGroup and sendToGroup. Possible values are: "sendToGroup" or "joinLeaveGroup". :type permission: str @@ -1960,7 +1889,7 @@ def grant_permission( request = build_grant_permission_request( - hub=hub, + hub=self._config.hub, permission=permission, connection_id=connection_id, api_version=api_version, @@ -1988,7 +1917,6 @@ def grant_permission( @distributed_trace def revoke_permission( self, - hub, # type: str permission, # type: str connection_id, # type: str **kwargs # type: Any @@ -1998,9 +1926,6 @@ def revoke_permission( Revoke permission for the connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param permission: The permission: current supported actions are joinLeaveGroup and sendToGroup. Possible values are: "sendToGroup" or "joinLeaveGroup". :type permission: str @@ -2027,7 +1952,7 @@ def revoke_permission( request = build_revoke_permission_request( - hub=hub, + hub=self._config.hub, permission=permission, connection_id=connection_id, api_version=api_version, @@ -2055,7 +1980,6 @@ def revoke_permission( @distributed_trace def has_permission( self, - hub, # type: str permission, # type: str connection_id, # type: str **kwargs # type: Any @@ -2065,9 +1989,6 @@ def has_permission( Check if a connection has permission to the specified action. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param permission: The permission: current supported actions are joinLeaveGroup and sendToGroup. Possible values are: "sendToGroup" or "joinLeaveGroup". :type permission: str @@ -2094,7 +2015,7 @@ def has_permission( request = build_has_permission_request( - hub=hub, + hub=self._config.hub, permission=permission, connection_id=connection_id, api_version=api_version, diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_patch.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_patch.py index 764b04b9f354..474d63fd7ead 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_patch.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_patch.py @@ -212,6 +212,8 @@ class WebPubSubServiceClientConfiguration(Configuration): Note that all parameters used to create this instance are saved as instance attributes. + :param hub: Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore. + :type hub: str :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str :param credential: Credential needed for the client to connect to Azure. @@ -221,6 +223,7 @@ class WebPubSubServiceClientConfiguration(Configuration): def __init__( self, + hub, # type: str endpoint, # type: str credential, # type: Union[TokenCredential, AzureKeyCredential] **kwargs # type: Any @@ -229,11 +232,14 @@ def __init__( super(WebPubSubServiceClientConfiguration, self).__init__(**kwargs) api_version = kwargs.pop('api_version', "2021-10-01") # type: str + if hub is None: + raise ValueError("Parameter 'hub' must not be None.") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") if credential is None: raise ValueError("Parameter 'credential' must not be None.") + self.hub = hub self.endpoint = endpoint self.credential = credential self.api_version = api_version @@ -267,6 +273,9 @@ class WebPubSubServiceClient(GeneratedWebPubSubServiceClient): :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str + :param hub: Target hub name, which should start with alphabetic characters and only contain + alpha-numeric characters or underscore. + :type hub: str :param credential: Credential needed for the client to connect to Azure. :type credential: ~azure.core.credentials.TokenCredential :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -277,6 +286,7 @@ class WebPubSubServiceClient(GeneratedWebPubSubServiceClient): def __init__( self, endpoint, # type: str + hub, # type: str credential, # type: Union[TokenCredential, AzureKeyCredential] **kwargs # type: Any ): @@ -287,7 +297,7 @@ def __init__( endpoint = endpoint + ":{}".format(kwargs.pop('port')) kwargs['origin_endpoint'] = endpoint _endpoint = '{Endpoint}' - self._config = WebPubSubServiceClientConfiguration(endpoint, credential, **kwargs) + self._config = WebPubSubServiceClientConfiguration(hub=hub, endpoint=endpoint, credential=credential, **kwargs) self._client = PipelineClient(base_url=_endpoint, config=self._config, **kwargs) self._serialize = Serializer() @@ -295,26 +305,27 @@ def __init__( self._serialize.client_side_validation = False @classmethod - def from_connection_string(cls, connection_string, **kwargs): - # type: (Type[ClientType], str, Any) -> ClientType + def from_connection_string(cls, connection_string, hub, **kwargs): + # type: (Type[ClientType], str, str, Any) -> ClientType """Create a new WebPubSubServiceClient from a connection string. :param connection_string: Connection string :type connection_string: str + :param hub: Target hub name, which should start with alphabetic characters and only contain + alpha-numeric characters or underscore. + :type hub: str :rtype: WebPubSubServiceClient """ kwargs = _parse_connection_string(connection_string, **kwargs) credential = AzureKeyCredential(kwargs.pop("accesskey")) - return cls(credential=credential, **kwargs) + return cls(hub=hub, credential=credential, **kwargs) @distributed_trace - def get_client_access_token(self, hub, **kwargs): + def get_client_access_token(self, **kwargs): # type: (str, Any) -> Dict[Any] """Build an authentication token. - :keyword hub: The hub to give access to. - :type hub: str :keyword user_id: User Id. :paramtype user_id: str :keyword roles: Roles that the connection with the generated token will have. @@ -325,7 +336,7 @@ def get_client_access_token(self, hub, **kwargs): :rtype: ~dict Example: - >>> get_client_access_token(hub='theHub') + >>> get_client_access_token() { 'baseUrl': 'wss://contoso.com/api/webpubsub/client/hubs/theHub', 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ...', @@ -345,11 +356,12 @@ def get_client_access_token(self, hub, **kwargs): # Switch from http(s) to ws(s) scheme client_endpoint = "ws" + endpoint[4:] + hub = self._config.hub client_url = "{}/client/hubs/{}".format(client_endpoint, hub) if isinstance(self._config.credential, AzureKeyCredential): token = _get_token_by_key(endpoint, hub, self._config.credential.key, **kwargs) else: - token = super(WebPubSubServiceClient, self).get_client_access_token(hub, **kwargs).get('token') + token = super(WebPubSubServiceClient, self).get_client_access_token(**kwargs).get('token') return { "baseUrl": client_url, diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_web_pub_sub_service_client.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_web_pub_sub_service_client.py index 0c8702bf159f..9d96f87e1225 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_web_pub_sub_service_client.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/_web_pub_sub_service_client.py @@ -25,6 +25,9 @@ class WebPubSubServiceClient(WebPubSubServiceClientOperationsMixin): """WebPubSubServiceClient. + :param hub: Target hub name, which should start with alphabetic characters and only contain + alpha-numeric characters or underscore. + :type hub: str :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str :param credential: Credential needed for the client to connect to Azure. @@ -36,13 +39,14 @@ class WebPubSubServiceClient(WebPubSubServiceClientOperationsMixin): def __init__( self, + hub, # type: str endpoint, # type: str credential, # type: "TokenCredential" **kwargs # type: Any ): # type: (...) -> None _endpoint = '{Endpoint}' - self._config = WebPubSubServiceClientConfiguration(endpoint=endpoint, credential=credential, **kwargs) + self._config = WebPubSubServiceClientConfiguration(hub=hub, endpoint=endpoint, credential=credential, **kwargs) self._client = PipelineClient(base_url=_endpoint, config=self._config, **kwargs) self._serialize = Serializer() diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_configuration.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_configuration.py index c9c68386762d..24dea080d9bd 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_configuration.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_configuration.py @@ -24,6 +24,8 @@ class WebPubSubServiceClientConfiguration(Configuration): Note that all parameters used to create this instance are saved as instance attributes. + :param hub: Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore. + :type hub: str :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str :param credential: Credential needed for the client to connect to Azure. @@ -34,6 +36,7 @@ class WebPubSubServiceClientConfiguration(Configuration): def __init__( self, + hub: str, endpoint: str, credential: "AsyncTokenCredential", **kwargs: Any @@ -41,11 +44,14 @@ def __init__( super(WebPubSubServiceClientConfiguration, self).__init__(**kwargs) api_version = kwargs.pop('api_version', "2021-10-01") # type: str + if hub is None: + raise ValueError("Parameter 'hub' must not be None.") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") if credential is None: raise ValueError("Parameter 'credential' must not be None.") + self.hub = hub self.endpoint = endpoint self.credential = credential self.api_version = api_version diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_operations/_operations.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_operations/_operations.py index ebd1f4541681..0fe699744e5b 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_operations/_operations.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_operations/_operations.py @@ -25,7 +25,6 @@ class WebPubSubServiceClientOperationsMixin: @distributed_trace_async async def get_client_access_token( self, - hub: str, *, user_id: Optional[str] = None, roles: Optional[List[str]] = None, @@ -36,9 +35,6 @@ async def get_client_access_token( Generate token for the client to connect Azure Web PubSub service. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :keyword user_id: User Id. :paramtype user_id: str :keyword roles: Roles that the connection with the generated token will have. @@ -70,7 +66,7 @@ async def get_client_access_token( request = build_get_client_access_token_request( - hub=hub, + hub=self._config.hub, api_version=api_version, user_id=user_id, roles=roles, @@ -105,7 +101,6 @@ async def get_client_access_token( @distributed_trace_async async def close_all_connections( self, - hub: str, *, excluded: Optional[List[str]] = None, reason: Optional[str] = None, @@ -115,9 +110,6 @@ async def close_all_connections( Close the connections in the hub. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :keyword excluded: Exclude these connectionIds when closing the connections in the hub. :paramtype excluded: list[str] :keyword reason: The reason closing the client connection. @@ -139,7 +131,7 @@ async def close_all_connections( request = build_close_all_connections_request( - hub=hub, + hub=self._config.hub, api_version=api_version, excluded=excluded, reason=reason, @@ -166,7 +158,6 @@ async def close_all_connections( @distributed_trace_async async def send_to_all( self, - hub: str, message: Union[IO, str, JSONType], *, excluded: Optional[List[str]] = None, @@ -176,9 +167,6 @@ async def send_to_all( Broadcast content inside request body to all the connected client connections. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param message: The payload body. :type message: IO or str or JSONType :keyword excluded: Excluded connection Ids. @@ -215,7 +203,7 @@ async def send_to_all( ) request = build_send_to_all_request( - hub=hub, + hub=self._config.hub, api_version=api_version, content_type=content_type, json=json, @@ -244,7 +232,6 @@ async def send_to_all( @distributed_trace_async async def connection_exists( self, - hub: str, connection_id: str, **kwargs: Any ) -> bool: @@ -252,9 +239,6 @@ async def connection_exists( Check if the connection with the given connectionId exists. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param connection_id: The connection Id. :type connection_id: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -274,7 +258,7 @@ async def connection_exists( request = build_connection_exists_request( - hub=hub, + hub=self._config.hub, connection_id=connection_id, api_version=api_version, template_url=self.connection_exists.metadata['url'], @@ -301,7 +285,6 @@ async def connection_exists( @distributed_trace_async async def close_connection( self, - hub: str, connection_id: str, *, reason: Optional[str] = None, @@ -311,9 +294,6 @@ async def close_connection( Close the client connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param connection_id: Target connection Id. :type connection_id: str :keyword reason: The reason closing the client connection. @@ -335,7 +315,7 @@ async def close_connection( request = build_close_connection_request( - hub=hub, + hub=self._config.hub, connection_id=connection_id, api_version=api_version, reason=reason, @@ -362,7 +342,6 @@ async def close_connection( @distributed_trace_async async def send_to_connection( self, - hub: str, connection_id: str, message: Union[IO, str, JSONType], **kwargs: Any @@ -371,9 +350,6 @@ async def send_to_connection( Send content inside request body to the specific connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param connection_id: The connection Id. :type connection_id: str :param message: The payload body. @@ -410,7 +386,7 @@ async def send_to_connection( ) request = build_send_to_connection_request( - hub=hub, + hub=self._config.hub, connection_id=connection_id, api_version=api_version, content_type=content_type, @@ -439,7 +415,6 @@ async def send_to_connection( @distributed_trace_async async def group_exists( self, - hub: str, group: str, **kwargs: Any ) -> bool: @@ -447,9 +422,6 @@ async def group_exists( Check if there are any client connections inside the given group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -469,7 +441,7 @@ async def group_exists( request = build_group_exists_request( - hub=hub, + hub=self._config.hub, group=group, api_version=api_version, template_url=self.group_exists.metadata['url'], @@ -496,7 +468,6 @@ async def group_exists( @distributed_trace_async async def close_group_connections( self, - hub: str, group: str, *, excluded: Optional[List[str]] = None, @@ -507,9 +478,6 @@ async def close_group_connections( Close connections in the specific group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :keyword excluded: Exclude these connectionIds when closing the connections in the group. @@ -533,7 +501,7 @@ async def close_group_connections( request = build_close_group_connections_request( - hub=hub, + hub=self._config.hub, group=group, api_version=api_version, excluded=excluded, @@ -561,7 +529,6 @@ async def close_group_connections( @distributed_trace_async async def send_to_group( self, - hub: str, group: str, message: Union[IO, str, JSONType], *, @@ -572,9 +539,6 @@ async def send_to_group( Send content inside request body to a group of connections. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param message: The payload body. @@ -613,7 +577,7 @@ async def send_to_group( ) request = build_send_to_group_request( - hub=hub, + hub=self._config.hub, group=group, api_version=api_version, content_type=content_type, @@ -643,7 +607,6 @@ async def send_to_group( @distributed_trace_async async def add_connection_to_group( self, - hub: str, group: str, connection_id: str, **kwargs: Any @@ -652,9 +615,6 @@ async def add_connection_to_group( Add a connection to the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param connection_id: Target connection Id. @@ -676,7 +636,7 @@ async def add_connection_to_group( request = build_add_connection_to_group_request( - hub=hub, + hub=self._config.hub, group=group, connection_id=connection_id, api_version=api_version, @@ -703,7 +663,6 @@ async def add_connection_to_group( @distributed_trace_async async def remove_connection_from_group( self, - hub: str, group: str, connection_id: str, **kwargs: Any @@ -712,9 +671,6 @@ async def remove_connection_from_group( Remove a connection from the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param connection_id: Target connection Id. @@ -736,7 +692,7 @@ async def remove_connection_from_group( request = build_remove_connection_from_group_request( - hub=hub, + hub=self._config.hub, group=group, connection_id=connection_id, api_version=api_version, @@ -763,7 +719,6 @@ async def remove_connection_from_group( @distributed_trace_async async def user_exists( self, - hub: str, user_id: str, **kwargs: Any ) -> bool: @@ -771,9 +726,6 @@ async def user_exists( Check if there are any client connections connected for the given user. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: Target user Id. :type user_id: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -793,7 +745,7 @@ async def user_exists( request = build_user_exists_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, template_url=self.user_exists.metadata['url'], @@ -820,7 +772,6 @@ async def user_exists( @distributed_trace_async async def close_user_connections( self, - hub: str, user_id: str, *, excluded: Optional[List[str]] = None, @@ -831,9 +782,6 @@ async def close_user_connections( Close connections for the specific user. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: The user Id. :type user_id: str :keyword excluded: Exclude these connectionIds when closing the connections for the user. @@ -857,7 +805,7 @@ async def close_user_connections( request = build_close_user_connections_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, excluded=excluded, @@ -885,7 +833,6 @@ async def close_user_connections( @distributed_trace_async async def send_to_user( self, - hub: str, user_id: str, message: Union[IO, str, JSONType], **kwargs: Any @@ -894,9 +841,6 @@ async def send_to_user( Send content inside request body to the specific user. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: The user Id. :type user_id: str :param message: The payload body. @@ -933,7 +877,7 @@ async def send_to_user( ) request = build_send_to_user_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, content_type=content_type, @@ -962,7 +906,6 @@ async def send_to_user( @distributed_trace_async async def add_user_to_group( self, - hub: str, group: str, user_id: str, **kwargs: Any @@ -971,9 +914,6 @@ async def add_user_to_group( Add a user to the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param user_id: Target user Id. @@ -995,7 +935,7 @@ async def add_user_to_group( request = build_add_user_to_group_request( - hub=hub, + hub=self._config.hub, group=group, user_id=user_id, api_version=api_version, @@ -1022,7 +962,6 @@ async def add_user_to_group( @distributed_trace_async async def remove_user_from_group( self, - hub: str, group: str, user_id: str, **kwargs: Any @@ -1031,9 +970,6 @@ async def remove_user_from_group( Remove a user from the target group. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param group: Target group name, which length should be greater than 0 and less than 1025. :type group: str :param user_id: Target user Id. @@ -1055,7 +991,7 @@ async def remove_user_from_group( request = build_remove_user_from_group_request( - hub=hub, + hub=self._config.hub, group=group, user_id=user_id, api_version=api_version, @@ -1082,7 +1018,6 @@ async def remove_user_from_group( @distributed_trace_async async def remove_user_from_all_groups( self, - hub: str, user_id: str, **kwargs: Any ) -> None: @@ -1090,9 +1025,6 @@ async def remove_user_from_all_groups( Remove a user from all groups. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param user_id: Target user Id. :type user_id: str :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -1112,7 +1044,7 @@ async def remove_user_from_all_groups( request = build_remove_user_from_all_groups_request( - hub=hub, + hub=self._config.hub, user_id=user_id, api_version=api_version, template_url=self.remove_user_from_all_groups.metadata['url'], @@ -1138,7 +1070,6 @@ async def remove_user_from_all_groups( @distributed_trace_async async def grant_permission( self, - hub: str, permission: str, connection_id: str, *, @@ -1149,9 +1080,6 @@ async def grant_permission( Grant permission to the connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param permission: The permission: current supported actions are joinLeaveGroup and sendToGroup. Possible values are: "sendToGroup" or "joinLeaveGroup". :type permission: str @@ -1177,7 +1105,7 @@ async def grant_permission( request = build_grant_permission_request( - hub=hub, + hub=self._config.hub, permission=permission, connection_id=connection_id, api_version=api_version, @@ -1205,7 +1133,6 @@ async def grant_permission( @distributed_trace_async async def revoke_permission( self, - hub: str, permission: str, connection_id: str, *, @@ -1216,9 +1143,6 @@ async def revoke_permission( Revoke permission for the connection. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param permission: The permission: current supported actions are joinLeaveGroup and sendToGroup. Possible values are: "sendToGroup" or "joinLeaveGroup". :type permission: str @@ -1244,7 +1168,7 @@ async def revoke_permission( request = build_revoke_permission_request( - hub=hub, + hub=self._config.hub, permission=permission, connection_id=connection_id, api_version=api_version, @@ -1272,7 +1196,6 @@ async def revoke_permission( @distributed_trace_async async def has_permission( self, - hub: str, permission: str, connection_id: str, *, @@ -1283,9 +1206,6 @@ async def has_permission( Check if a connection has permission to the specified action. - :param hub: Target hub name, which should start with alphabetic characters and only contain - alpha-numeric characters or underscore. - :type hub: str :param permission: The permission: current supported actions are joinLeaveGroup and sendToGroup. Possible values are: "sendToGroup" or "joinLeaveGroup". :type permission: str @@ -1311,7 +1231,7 @@ async def has_permission( request = build_has_permission_request( - hub=hub, + hub=self._config.hub, permission=permission, connection_id=connection_id, api_version=api_version, diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_patch.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_patch.py index 31776c93bdfb..dadce57fcb5c 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_patch.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_patch.py @@ -63,6 +63,8 @@ class WebPubSubServiceClientConfiguration(Configuration): Note that all parameters used to create this instance are saved as instance attributes. + :param hub: Target hub name, which should start with alphabetic characters and only contain alpha-numeric characters or underscore. + :type hub: str :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str :param credential: Credential needed for the client to connect to Azure. @@ -73,6 +75,7 @@ class WebPubSubServiceClientConfiguration(Configuration): def __init__( self, + hub: str, endpoint: str, credential: Union["AsyncTokenCredential", "AzureKeyCredential"], **kwargs: Any @@ -84,11 +87,14 @@ def __init__( endpoint = endpoint + ":{}".format(kwargs.pop('port')) api_version = kwargs.pop('api_version', "2021-10-01") # type: str + if hub is None: + raise ValueError("Parameter 'hub' must not be None.") if endpoint is None: raise ValueError("Parameter 'endpoint' must not be None.") if credential is None: raise ValueError("Parameter 'credential' must not be None.") + self.hub = hub self.endpoint = endpoint self.credential = credential self.api_version = api_version @@ -121,6 +127,9 @@ class WebPubSubServiceClient(GeneratedWebPubSubServiceClient): :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str + :param hub: Target hub name, which should start with alphabetic characters and only contain + alpha-numeric characters or underscore. + :type hub: str :param credential: Credential needed for the client to connect to Azure. :type credential: ~azure.core.credentials_async.AsyncTokenCredential :keyword api_version: Api Version. The default value is "2021-10-01". Note that overriding this @@ -131,12 +140,13 @@ class WebPubSubServiceClient(GeneratedWebPubSubServiceClient): def __init__( self, endpoint: str, + hub: str, credential: Union["AsyncTokenCredential", "AzureKeyCredential"], **kwargs: Any ) -> None: kwargs['origin_endpoint'] = endpoint _endpoint = '{Endpoint}' - self._config = WebPubSubServiceClientConfiguration(endpoint, credential, **kwargs) + self._config = WebPubSubServiceClientConfiguration(hub=hub, endpoint=endpoint, credential=credential, **kwargs) self._client = AsyncPipelineClient(base_url=_endpoint, config=self._config, **kwargs) self._serialize = Serializer() @@ -144,25 +154,26 @@ def __init__( self._serialize.client_side_validation = False @classmethod - def from_connection_string(cls, connection_string, **kwargs): - # type: (Type[ClientType], str, Any) -> ClientType + def from_connection_string(cls, connection_string, hub, **kwargs): + # type: (Type[ClientType], str, str, Any) -> ClientType """Create a new WebPubSubServiceClient from a connection string. :param connection_string: Connection string :type connection_string: ~str + :param hub: Target hub name, which should start with alphabetic characters and only contain + alpha-numeric characters or underscore. + :type hub: str :rtype: WebPubSubServiceClient """ kwargs = _parse_connection_string(connection_string, **kwargs) credential = AzureKeyCredential(kwargs.pop("accesskey")) - return cls(credential=credential, **kwargs) + return cls(hub=hub, credential=credential, **kwargs) @distributed_trace_async - async def get_client_access_token(self, hub, **kwargs): + async def get_client_access_token(self, **kwargs): # type: (str, Any) -> Dict[Any] """Build an authentication token. - :keyword hub: The hub to give access to. - :type hub: str :keyword user_id: User Id. :paramtype user_id: str :keyword roles: Roles that the connection with the generated token will have. @@ -173,7 +184,7 @@ async def get_client_access_token(self, hub, **kwargs): :rtype: ~dict Example: - >>> get_client_access_token(hub='theHub') + >>> get_client_access_token() { 'baseUrl': 'wss://contoso.com/api/webpubsub/client/hubs/theHub', 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ...', @@ -193,11 +204,12 @@ async def get_client_access_token(self, hub, **kwargs): # Switch from http(s) to ws(s) scheme client_endpoint = "ws" + endpoint[4:] + hub = self._config.hub client_url = "{}/client/hubs/{}".format(client_endpoint, hub) if isinstance(self._config.credential, AzureKeyCredential): token = _get_token_by_key(endpoint, hub, self._config.credential.key, **kwargs) else: - access_token = await super().get_client_access_token(hub, **kwargs) + access_token = await super().get_client_access_token(**kwargs) token = access_token.get('token') return { diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_web_pub_sub_service_client.py b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_web_pub_sub_service_client.py index 6ee270bc5bf6..340adac20cc3 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_web_pub_sub_service_client.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/azure/messaging/webpubsubservice/aio/_web_pub_sub_service_client.py @@ -25,6 +25,9 @@ class WebPubSubServiceClient(WebPubSubServiceClientOperationsMixin): """WebPubSubServiceClient. + :param hub: Target hub name, which should start with alphabetic characters and only contain + alpha-numeric characters or underscore. + :type hub: str :param endpoint: HTTP or HTTPS endpoint for the Web PubSub service instance. :type endpoint: str :param credential: Credential needed for the client to connect to Azure. @@ -36,12 +39,13 @@ class WebPubSubServiceClient(WebPubSubServiceClientOperationsMixin): def __init__( self, + hub: str, endpoint: str, credential: "AsyncTokenCredential", **kwargs: Any ) -> None: _endpoint = '{Endpoint}' - self._config = WebPubSubServiceClientConfiguration(endpoint=endpoint, credential=credential, **kwargs) + self._config = WebPubSubServiceClientConfiguration(hub=hub, endpoint=endpoint, credential=credential, **kwargs) self._client = AsyncPipelineClient(base_url=_endpoint, config=self._config, **kwargs) self._serialize = Serializer() diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/Readme.md b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/Readme.md new file mode 100644 index 000000000000..a87a9d447d4b --- /dev/null +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/Readme.md @@ -0,0 +1,51 @@ +--- +page_type: sample +languages: + - python +products: + - azure + - azure-messaging-webpubsubservice +urlFragment: azure-messaging-webpubsubservice-samples +--- + +# Azure Web PubSub service client library for Python Samples + +These are code samples that show common scenario operations with the Azure Storage Blob client library. +The async versions of the samples (the python sample files appended with `_async`) show asynchronous operations, +and require Python 3.5 or later. + +## Prerequisites +* Python 2.7, or 3.5 or later is required to use this package (3.5 or later if using asyncio) +* You need an [Azure subscription][azure_sub], and a [Azure WebPubSub service instance][webpubsubservice_docs] to use this package. + +## Setup + +1. Install the Azure Web PubSub service client library for Python with [pip](https://pypi.org/project/pip/): + +```bash +pip install azure-messaging-webpubsubservice +``` + +2. Clone or download this sample repository +3. Open the sample folder in Visual Studio Code or your IDE of choice. + +## Running the samples + +1. Open a terminal window and `cd` to the directory that the samples are saved in. +2. Set the environment variables specified in the sample file you wish to run. +3. Follow the usage described in the file, e.g. `python get_client_access_token.py` + +## More samples + +Check [more samples here][servicesample]. + +## Next Steps + +Take a look at our [API Documentation][apiref] for more information about the APIs that are available in the clients. + + + +[azure_sub]: https://azure.microsoft.com/free/ +[webpubsubservice_docs]: https://aka.ms/awps/doc +[servicesample]: https://github.com/Azure/azure-webpubsub/tree/main/samples/python +[apiref]: https://aka.ms/awps/sdk/python diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token.py b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token.py index 74a2de3c890d..561ea26a3c23 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token.py @@ -43,15 +43,15 @@ exit() # Build a client through AAD -client_aad = WebPubSubServiceClient(credential=DefaultAzureCredential(), endpoint=endpoint) +client_aad = WebPubSubServiceClient(credential=DefaultAzureCredential(), endpoint=endpoint, hub='hub') # Build authentication token -token_aad = client_aad.get_client_access_token(hub='hub') +token_aad = client_aad.get_client_access_token() print('token by AAD: {}'.format(token_aad)) # Build a client through connection string -client_key = WebPubSubServiceClient.from_connection_string(connection_string) +client_key = WebPubSubServiceClient.from_connection_string(connection_string, hub='hub') # Build authentication token -token_key = client_key.get_client_access_token(hub='hub') +token_key = client_key.get_client_access_token() print('token by access key: {}'.format(token_key)) diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token_async.py b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token_async.py index 7f010812cb00..b3b354d60eeb 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token_async.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/get_client_access_token_async.py @@ -47,15 +47,15 @@ async def main(): # Build a client through AAD(async) async with DefaultAzureCredential() as credential: - async with WebPubSubServiceClientAsync(credential=credential, endpoint=endpoint) as client_aad_async: + async with WebPubSubServiceClientAsync(credential=credential, endpoint=endpoint, hub='hub') as client_aad_async: # Build authentication token(async) - token_aad_async = await client_aad_async.get_client_access_token(hub='hub') + token_aad_async = await client_aad_async.get_client_access_token() print('token by AAD(async): {}'.format(token_aad_async)) # Build a client through connection string(async) - async with WebPubSubServiceClientAsync.from_connection_string(connection_string) as client_key_async: + async with WebPubSubServiceClientAsync.from_connection_string(connection_string, hub='hub') as client_key_async: # Build authentication token(async) - token_key_async = await client_key_async.get_client_access_token(hub='hub') + token_key_async = await client_key_async.get_client_access_token() print('token by access key(async): {}'.format(token_key_async)) if __name__ == '__main__': diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad.py b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad.py index 3aea2b863fcd..b84b2c51c19e 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad.py @@ -44,12 +44,12 @@ exit() # Build a client through AAD -client = WebPubSubServiceClient(credential=DefaultAzureCredential(), endpoint=endpoint) +client = WebPubSubServiceClient(credential=DefaultAzureCredential(), endpoint=endpoint, hub='hub') # Send a json message to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message={'Hello': 'all'}) + client.send_to_all(message={'Hello': 'all'}) print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) @@ -57,7 +57,7 @@ # Send a text message to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message='hello, text!', content_type='text/plain') + client.send_to_all(message='hello, text!', content_type='text/plain') print('Successfully sent a text message') except HttpResponseError as e: print('Failed to send text message: {}'.format(e.response.json())) @@ -66,7 +66,7 @@ # Send a json message from a stream to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message=io.BytesIO(b'{ "hello": "world" }'), content_type='application/octet-stream') + client.send_to_all(message=io.BytesIO(b'{ "hello": "world" }'), content_type='application/octet-stream') print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad_apim_proxy.py b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad_apim_proxy.py index d5acc6d8938a..662f1c4f94d9 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad_apim_proxy.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_aad_apim_proxy.py @@ -46,12 +46,12 @@ # Build a client through AAD # If you want to know more about the effect of `reverse_proxy_endpoint`, please reference: https://github.com/Azure/azure-webpubsub/issues/194 -client = WebPubSubServiceClient(credential=DefaultAzureCredential(), endpoint=endpoint, reverse_proxy_endpoint=reverse_proxy_endpoint) +client = WebPubSubServiceClient(credential=DefaultAzureCredential(), endpoint=endpoint, hub='hub', reverse_proxy_endpoint=reverse_proxy_endpoint) # Send a json message to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message={'Hello': 'reverse_proxy_endpoint!'}) + client.send_to_all(message={'Hello': 'reverse_proxy_endpoint!'}) print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) @@ -59,7 +59,7 @@ # Send a text message to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message='hello, reverse_proxy_endpoint!', content_type='text/plain') + client.send_to_all(message='hello, reverse_proxy_endpoint!', content_type='text/plain') print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) @@ -68,7 +68,7 @@ # Send a json message from a stream to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message=io.BytesIO(b'{ "hello": "reverse_proxy_endpoint" }'), content_type='application/octet-stream') + client.send_to_all(message=io.BytesIO(b'{ "hello": "reverse_proxy_endpoint" }'), content_type='application/octet-stream') print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string.py b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string.py index ab24eec4c48b..124fded6b5f7 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string.py @@ -42,11 +42,11 @@ # Build a client from the connection string. And for this example, we have enabled debug # tracing. For production code, this should be turned off. -client = WebPubSubServiceClient.from_connection_string(connection_string, logging_enable=True) +client = WebPubSubServiceClient.from_connection_string(connection_string, hub='hub', logging_enable=True) try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message={'Hello': 'all!'}) + client.send_to_all(message={'Hello': 'all!'}) print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) @@ -54,7 +54,7 @@ # Send a text message to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message='hello, text!', content_type='text/plain') + client.send_to_all(message='hello, text!', content_type='text/plain') print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) @@ -63,7 +63,7 @@ # Send a json message from a stream to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message=io.BytesIO(b'{ "hello": "world" }'), content_type='application/octet-stream') + client.send_to_all(message=io.BytesIO(b'{ "hello": "world" }'), content_type='application/octet-stream') print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string_apim_proxy.py b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string_apim_proxy.py index cf3b0ed94d04..2025dc7f838c 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string_apim_proxy.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/samples/send_messages_connection_string_apim_proxy.py @@ -44,11 +44,11 @@ # Build a client from the connection string. And for this example, we have enabled debug # tracing. For production code, this should be turned off. # If you want to know more about the effect of `reverse_proxy_endpoint`, please reference: https://github.com/Azure/azure-webpubsub/issues/194 -client = WebPubSubServiceClient.from_connection_string(connection_string, logging_enable=True, reverse_proxy_endpoint=reverse_proxy_endpoint) +client = WebPubSubServiceClient.from_connection_string(connection_string, hub='hub', logging_enable=True, reverse_proxy_endpoint=reverse_proxy_endpoint) try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message={'Hello': 'connection_string_reverse_proxy!'}) + client.send_to_all(message={'Hello': 'connection_string_reverse_proxy!'}) print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) @@ -56,7 +56,7 @@ # Send a text message to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message='hello, connection_string_reverse_proxy!', content_type='text/plain') + client.send_to_all(message='hello, connection_string_reverse_proxy!', content_type='text/plain') print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) @@ -65,7 +65,7 @@ # Send a json message from a stream to everybody on the given hub... try: # Raise an exception if the service rejected the call - client.send_to_all('Hub', message=io.BytesIO(b'{ "hello": "connection_string_reverse_proxy" }'), content_type='application/octet-stream') + client.send_to_all(message=io.BytesIO(b'{ "hello": "connection_string_reverse_proxy" }'), content_type='application/octet-stream') print('Successfully sent a JSON message') except HttpResponseError as e: print('Failed to send JSON message: {}'.format(e.response.json())) diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/setup.py b/sdk/webpubsub/azure-messaging-webpubsubservice/setup.py index 6a34ea471e9e..984e0e81b5d9 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/setup.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/setup.py @@ -39,7 +39,7 @@ license="MIT License", author="Microsoft Corporation", author_email="azpysdkhelp@microsoft.com", - url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/signalr/azure-messaging-webpubsubservice", + url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/webpubsub/azure-messaging-webpubsubservice", classifiers=[ "Development Status :: 4 - Beta", "Programming Language :: Python", diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/swagger/README.md b/sdk/webpubsub/azure-messaging-webpubsubservice/swagger/README.md index d9baae2ca7e4..1e36db4cab71 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/swagger/README.md +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/swagger/README.md @@ -82,4 +82,166 @@ directive: - from: swagger-document where: $["paths"]["/api/hubs/{hub}/:generateToken"].post transform: $["operationId"] = "GetClientAccessToken" -``` \ No newline at end of file +``` + +### Add hub to client on generate token + +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:generateToken"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### SendToAll + +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:send"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### ConnectionExistsImpl +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/connections/{connectionId}"].head.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### CloseConnection +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/connections/{connectionId}"].delete.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### SendToConnection +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/connections/{connectionId}/:send"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### GroupExistsImpl +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/groups/{group}"].head.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### SendToGroup +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/groups/{group}/:send"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### AddConnectionToGroup +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/groups/{group}/connections/{connectionId}"].put.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### RemoveConnectionFromGroup +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/groups/{group}/connections/{connectionId}"].delete.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### UserExistsImpl +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/users/{userId}"].head.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### SendToUser +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/users/{userId}/:send"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### AddUserToGroup +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/users/{userId}/groups/{group}"].put.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### RemoveUserFromGroup +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/users/{userId}/groups/{group}"].delete.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### RemoveUserFromAllGroups +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/users/{userId}/groups"].delete.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### GrantPermission +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/permissions/{permission}/connections/{connectionId}"].put.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### RevokePermission +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/permissions/{permission}/connections/{connectionId}"].delete.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### CheckPermission +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/permissions/{permission}/connections/{connectionId}"].head.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### CloseAllConnections +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:closeConnections"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### CloseGroupConnections +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/groups/{group}/:closeConnections"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` + +### CloseUserConnections +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/users/{userId}/:closeConnections"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +``` diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_get_client_access_token.yaml b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_get_client_access_token.yaml index 05cdb35df965..21b9680f4384 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_get_client_access_token.yaml +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_get_client_access_token.yaml @@ -11,13 +11,15 @@ interactions: Content-Length: - '0' User-Agent: - - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.9.5 (macOS-11.6-x86_64-i386-64bit) + - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.7.3 (Windows-10-10.0.19041-SP0) method: POST uri: https://myservice.webpubsub.azure.com/api/hubs/hub/:generateToken?minutesToExpire=60&api-version=2021-10-01 response: body: - string: '{"token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InMtYTdhODUzZjMtZGE1NS00MGU5LWE0OGEtZTVhNTcxN2U4Yjk4IiwidHlwIjoiSldUIn0.eyJuYmYiOjE2MzY0MDczMjIsImV4cCI6MTYzNjQxMDkyMiwiaWF0IjoxNjM2NDA3MzIyLCJhdWQiOiJodHRwczovL3dlYnB1YnN1Yi15eWMud2VicHVic3ViLmF6dXJlLmNvbS9jbGllbnQvaHVicy9odWIifQ.MXXNoxwYlH5LVLeyWkPQU8E_Y0m1WXIBE5JWy2xFyXs"}' + string: '{"token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InMtYTdhODUzZjMtZGE1NS00MGU5LWE0OGEtZTVhNTcxN2U4Yjk4IiwidHlwIjoiSldUIn0.eyJuYmYiOjE2MzY1MTYyNzEsImV4cCI6MTYzNjUxOTg3MSwiaWF0IjoxNjM2NTE2MjcxLCJhdWQiOiJodHRwczovL3dlYnB1YnN1Yi15eWMud2VicHVic3ViLmF6dXJlLmNvbS9jbGllbnQvaHVicy9odWIifQ.Wn9-TPRUG54A0GpMUwah86XG8yQxvH8HcQincCf7pAE"}' headers: + api-supported-versions: + - '2021-10-01' connection: - keep-alive content-length: @@ -25,7 +27,7 @@ interactions: content-type: - application/json; charset=utf-8 date: - - Mon, 08 Nov 2021 21:35:22 GMT + - Wed, 10 Nov 2021 03:51:11 GMT strict-transport-security: - max-age=15724800; includeSubDomains transfer-encoding: diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_request.yaml b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_request.yaml index f295ab2a4034..05404f3c3963 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_request.yaml +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_request.yaml @@ -13,19 +13,21 @@ interactions: Content-Type: - text/plain User-Agent: - - azsdk-python-messaging-webpubsubservice/1.0.0b3 Python/3.7.3 (Windows-10-10.0.19041-SP0) + - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.7.3 (Windows-10-10.0.19041-SP0) method: POST uri: https://myservice.webpubsub.azure.com/api/hubs/Hub/:send?api-version=2021-10-01 response: body: string: '' headers: + api-supported-versions: + - '2021-10-01' connection: - keep-alive content-length: - '0' date: - - Thu, 04 Nov 2021 03:43:19 GMT + - Wed, 10 Nov 2021 03:51:15 GMT strict-transport-security: - max-age=15724800; includeSubDomains status: diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all.yaml b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all.yaml index da7a63247a36..d6d6d0049321 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all.yaml +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all.yaml @@ -13,19 +13,21 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-messaging-webpubsubservice/1.0.0b3 Python/3.7.3 (Windows-10-10.0.19041-SP0) + - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.7.3 (Windows-10-10.0.19041-SP0) method: POST - uri: https://myservice.webpubsub.azure.com/api/hubs/Hub/:send?api-version=2021-10-01 + uri: https://myservice.webpubsub.azure.com/api/hubs/hub/:send?api-version=2021-10-01 response: body: string: '' headers: + api-supported-versions: + - '2021-10-01' connection: - keep-alive content-length: - '0' date: - - Thu, 04 Nov 2021 03:43:22 GMT + - Wed, 10 Nov 2021 03:51:17 GMT strict-transport-security: - max-age=15724800; includeSubDomains status: diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all_api_management_proxy.yaml b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all_api_management_proxy.yaml index 4cea0647a08d..142a7b0f15f7 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all_api_management_proxy.yaml +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke.test_webpubsub_send_to_all_api_management_proxy.yaml @@ -13,17 +13,19 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-messaging-webpubsubservice/1.0.0b3 Python/3.7.3 (Windows-10-10.0.19041-SP0) + - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.7.3 (Windows-10-10.0.19041-SP0) method: POST - uri: https://myservice.azure-api.net/api/hubs/Hub/:send?api-version=2021-10-01 + uri: https://myservice.azure-api.net/api/hubs/hub/:send?api-version=2021-10-01 response: body: string: '' headers: + api-supported-versions: + - '2021-10-01' content-length: - '0' date: - - Thu, 04 Nov 2021 03:43:25 GMT + - Wed, 10 Nov 2021 03:51:19 GMT strict-transport-security: - max-age=15724800; includeSubDomains status: diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_get_client_access_token.yaml b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_get_client_access_token.yaml index e7f32435a510..6551a36dc7c3 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_get_client_access_token.yaml +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_get_client_access_token.yaml @@ -5,16 +5,17 @@ interactions: Accept: - application/json, text/json User-Agent: - - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.9.5 (macOS-11.6-x86_64-i386-64bit) + - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.7.3 (Windows-10-10.0.19041-SP0) method: POST uri: https://myservice.webpubsub.azure.com/api/hubs/hub/:generateToken?minutesToExpire=60&api-version=2021-10-01 response: body: - string: '{"token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InMtYTdhODUzZjMtZGE1NS00MGU5LWE0OGEtZTVhNTcxN2U4Yjk4IiwidHlwIjoiSldUIn0.eyJuYmYiOjE2MzY0MDczMjMsImV4cCI6MTYzNjQxMDkyMywiaWF0IjoxNjM2NDA3MzIzLCJhdWQiOiJodHRwczovL3dlYnB1YnN1Yi15eWMud2VicHVic3ViLmF6dXJlLmNvbS9jbGllbnQvaHVicy9odWIifQ.XCY7TIIyvjJvlC3gY5-oXd7xX-3wNzGVv35Hsm0Bwts"}' + string: '{"token":"eyJhbGciOiJIUzI1NiIsImtpZCI6InMtYTdhODUzZjMtZGE1NS00MGU5LWE0OGEtZTVhNTcxN2U4Yjk4IiwidHlwIjoiSldUIn0.eyJuYmYiOjE2MzY1MTYyOTEsImV4cCI6MTYzNjUxOTg5MSwiaWF0IjoxNjM2NTE2MjkxLCJhdWQiOiJodHRwczovL3dlYnB1YnN1Yi15eWMud2VicHVic3ViLmF6dXJlLmNvbS9jbGllbnQvaHVicy9odWIifQ.B0e9vJguSn_tYlz48vV-4YeaeRnSz8LtCk3NLg2PZPY"}' headers: + api-supported-versions: '2021-10-01' connection: keep-alive content-type: application/json; charset=utf-8 - date: Mon, 08 Nov 2021 21:35:23 GMT + date: Wed, 10 Nov 2021 03:51:31 GMT strict-transport-security: max-age=15724800; includeSubDomains transfer-encoding: chunked vary: Accept-Encoding diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all.yaml b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all.yaml index 1d06e0e8b263..c64196a94947 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all.yaml +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all.yaml @@ -9,19 +9,20 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-messaging-webpubsubservice/1.0.0b3 Python/3.7.3 (Windows-10-10.0.19041-SP0) + - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.7.3 (Windows-10-10.0.19041-SP0) method: POST - uri: https://myservice.webpubsub.azure.com/api/hubs/Hub/:send?api-version=2021-10-01 + uri: https://myservice.webpubsub.azure.com/api/hubs/hub/:send?api-version=2021-10-01 response: body: string: '' headers: + api-supported-versions: '2021-10-01' connection: keep-alive content-length: '0' - date: Fri, 05 Nov 2021 05:03:57 GMT + date: Wed, 10 Nov 2021 03:51:33 GMT strict-transport-security: max-age=15724800; includeSubDomains status: code: 202 message: Accepted - url: https://webpubsub-yyc.webpubsub.azure.com/api/hubs/Hub/:send?api-version=2021-10-01 + url: https://webpubsub-yyc.webpubsub.azure.com/api/hubs/hub/:send?api-version=2021-10-01 version: 1 diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all_apim_proxy.yaml b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all_apim_proxy.yaml index 4818dba79a5d..d9eefa1e2bc7 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all_apim_proxy.yaml +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/recordings/test_smoke_async.test_webpubsub_send_to_all_apim_proxy.yaml @@ -9,18 +9,19 @@ interactions: Content-Type: - application/json User-Agent: - - azsdk-python-messaging-webpubsubservice/1.0.0b3 Python/3.7.3 (Windows-10-10.0.19041-SP0) + - azsdk-python-messaging-webpubsubservice/1.0.0 Python/3.7.3 (Windows-10-10.0.19041-SP0) method: POST - uri: https://myservice.azure-api.net/api/hubs/Hub/:send?api-version=2021-10-01 + uri: https://myservice.azure-api.net/api/hubs/hub/:send?api-version=2021-10-01 response: body: string: '' headers: + api-supported-versions: '2021-10-01' content-length: '0' - date: Fri, 05 Nov 2021 05:04:00 GMT + date: Wed, 10 Nov 2021 03:51:35 GMT strict-transport-security: max-age=15724800; includeSubDomains status: code: 202 message: Accepted - url: https://apimanagement-yyc.azure-api.net/api/hubs/Hub/:send?api-version=2021-10-01 + url: https://apimanagement-yyc.azure-api.net/api/hubs/hub/:send?api-version=2021-10-01 version: 1 diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_jwt_async.py b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_jwt_async.py index 0af8f8e434de..4e87c6912e2c 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_jwt_async.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_jwt_async.py @@ -35,7 +35,7 @@ def _decode_token(client, token): @pytest.mark.parametrize("connection_string,endpoint", test_cases) def test_parse_connection_string(connection_string, endpoint): - client = WebPubSubServiceClient.from_connection_string(connection_string) + client = WebPubSubServiceClient.from_connection_string(connection_string, "hub") assert client._config.endpoint == endpoint assert isinstance(client._config.credential, AzureKeyCredential) assert client._config.credential.key == access_key @@ -51,10 +51,10 @@ def test_parse_connection_string(connection_string, endpoint): @pytest.mark.asyncio async def test_generate_uri_contains_expected_payloads_dto(user_id, roles): client = WebPubSubServiceClient.from_connection_string( - f"Endpoint=http://localhost;Port=8080;AccessKey={access_key};Version=1.0;", + f"Endpoint=http://localhost;Port=8080;AccessKey={access_key};Version=1.0;", "hub" ) minutes_to_expire = 5 - token = await client.get_client_access_token(hub="hub", user_id=user_id, roles=roles, minutes_to_expire=minutes_to_expire) + token = await client.get_client_access_token(user_id=user_id, roles=roles, minutes_to_expire=minutes_to_expire) assert token assert len(token) == 3 assert set(token.keys()) == set(["baseUrl", "url", "token"]) @@ -84,9 +84,9 @@ async def test_generate_uri_contains_expected_payloads_dto(user_id, roles): @pytest.mark.parametrize("connection_string,hub,expected_url", test_cases) @pytest.mark.asyncio async def test_generate_url_use_same_kid_with_same_key(connection_string, hub, expected_url): - client = WebPubSubServiceClient.from_connection_string(connection_string) - url_1 = (await client.get_client_access_token(hub=hub))['url'] - url_2 = (await client.get_client_access_token(hub=hub))['url'] + client = WebPubSubServiceClient.from_connection_string(connection_string, hub) + url_1 = (await client.get_client_access_token())['url'] + url_2 = (await client.get_client_access_token())['url'] assert url_1.split("?")[0] == url_2.split("?")[0] == expected_url diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke.py b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke.py index f97f07a65baf..5a9b8fcabe10 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke.py @@ -15,17 +15,17 @@ class WebpubsubSmokeTest(WebpubsubTest): @WebpubsubPowerShellPreparer() def test_webpubsub_send_to_all(self, webpubsub_endpoint): - client = self.create_client(endpoint=webpubsub_endpoint) - client.send_to_all('Hub', {'hello': 'test_webpubsub_send_to_all'}) + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub') + client.send_to_all({'hello': 'test_webpubsub_send_to_all'}) @WebpubsubPowerShellPreparer() def test_webpubsub_send_to_all_api_management_proxy(self, webpubsub_endpoint, webpubsub_reverse_proxy_endpoint=None): - client = self.create_client(endpoint=webpubsub_endpoint, reverse_proxy_endpoint=webpubsub_reverse_proxy_endpoint) - client.send_to_all('Hub', {'hello': 'test_webpubsub_send_to_all_api_management_proxy'}) + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub', reverse_proxy_endpoint=webpubsub_reverse_proxy_endpoint) + client.send_to_all({'hello': 'test_webpubsub_send_to_all_api_management_proxy'}) @WebpubsubPowerShellPreparer() def test_webpubsub_send_request(self, webpubsub_endpoint): - client = self.create_client(endpoint=webpubsub_endpoint) + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub') request = build_send_to_all_request('Hub', content='test_webpubsub_send_request', content_type='text/plain') response = client.send_request(request) assert response.status_code == 202 @@ -33,14 +33,14 @@ def test_webpubsub_send_request(self, webpubsub_endpoint): # If reverse_proxy_endpoint is not available, `ServiceRequestError` will be raised @WebpubsubPowerShellPreparer() def test_webpubsub_send_to_all_api_management_proxy_counter_test(self, webpubsub_endpoint): - client = self.create_client(endpoint=webpubsub_endpoint, reverse_proxy_endpoint='https://example.azure-api.net') + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub', reverse_proxy_endpoint='https://example.azure-api.net') with pytest.raises(ServiceRequestError): - client.send_to_all('Hub', {'hello': 'test_webpubsub_send_to_all_api_management_proxy_counter_test'}) + client.send_to_all({'hello': 'test_webpubsub_send_to_all_api_management_proxy_counter_test'}) @WebpubsubPowerShellPreparer() def test_get_client_access_token(self, webpubsub_endpoint): - client = self.create_client(endpoint=webpubsub_endpoint) - access_token = client.get_client_access_token(hub='hub') + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub') + access_token = client.get_client_access_token() assert len(access_token) == 3 assert access_token['baseUrl'][:3] == "wss" assert access_token['token'] diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke_async.py b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke_async.py index 54003fbb010d..55a7270f6c25 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke_async.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/test_smoke_async.py @@ -5,25 +5,25 @@ # license information. # ------------------------------------------------------------------------- from testcase import WebpubsubPowerShellPreparer -from testcase_async import WebpubsubTestAsync +from testcase_async import WebpubsubAsyncTest -class WebpubsubSmokeTestAsync(WebpubsubTestAsync): +class WebpubsubSmokeAsyncTest(WebpubsubAsyncTest): @WebpubsubPowerShellPreparer() async def test_webpubsub_send_to_all(self, webpubsub_endpoint): - client = self.create_client(endpoint=webpubsub_endpoint) - await client.send_to_all('Hub', {'hello': 'test_webpubsub_send_to_all'}) + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub') + await client.send_to_all({'hello': 'test_webpubsub_send_to_all'}) @WebpubsubPowerShellPreparer() async def test_webpubsub_send_to_all_apim_proxy(self, webpubsub_endpoint, webpubsub_reverse_proxy_endpoint=None): - client = self.create_client(endpoint=webpubsub_endpoint, reverse_proxy_endpoint=webpubsub_reverse_proxy_endpoint) - await client.send_to_all('Hub', {'hello': 'test_webpubsub_send_to_all_apim_proxy'}) + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub', reverse_proxy_endpoint=webpubsub_reverse_proxy_endpoint) + await client.send_to_all({'hello': 'test_webpubsub_send_to_all_apim_proxy'}) @WebpubsubPowerShellPreparer() async def test_get_client_access_token(self, webpubsub_endpoint): - client = self.create_client(endpoint=webpubsub_endpoint) - access_token = await client.get_client_access_token(hub='hub') + client = self.create_client(endpoint=webpubsub_endpoint, hub='hub') + access_token = await client.get_client_access_token() assert len(access_token) == 3 assert access_token['baseUrl'][:3] == "wss" assert access_token['token'] diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase.py b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase.py index 3c7d541b88d2..3acc53983e83 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase.py @@ -13,14 +13,15 @@ class WebpubsubTest(AzureTestCase): def __init__(self, method_name, **kwargs): super(WebpubsubTest, self).__init__(method_name, **kwargs) - def create_client(self, endpoint=None, reverse_proxy_endpoint=None, **kwargs): + def create_client(self, endpoint=None, hub=None, reverse_proxy_endpoint=None, **kwargs): if kwargs.get("connection_string"): - return WebPubSubServiceClient.from_connection_string(kwargs.pop("connection_string")) + return WebPubSubServiceClient.from_connection_string(kwargs.pop("connection_string"), hub) credential = self.get_credential(WebPubSubServiceClient) return self.create_client_from_credential( WebPubSubServiceClient, credential=credential, endpoint=endpoint, + hub=hub, reverse_proxy_endpoint=reverse_proxy_endpoint ) diff --git a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase_async.py b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase_async.py index 46f9f1662b8e..9930e81b7628 100644 --- a/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase_async.py +++ b/sdk/webpubsub/azure-messaging-webpubsubservice/tests/testcase_async.py @@ -8,15 +8,16 @@ from azure.messaging.webpubsubservice.aio import WebPubSubServiceClient -class WebpubsubTestAsync(AzureTestCase): +class WebpubsubAsyncTest(AzureTestCase): def __init__(self, method_name, **kwargs): - super(WebpubsubTestAsync, self).__init__(method_name, **kwargs) + super(WebpubsubAsyncTest, self).__init__(method_name, **kwargs) - def create_client(self, endpoint, reverse_proxy_endpoint=None): + def create_client(self, endpoint, hub, reverse_proxy_endpoint=None): credential = self.get_credential(WebPubSubServiceClient, is_async=True) return self.create_client_from_credential( WebPubSubServiceClient, credential=credential, endpoint=endpoint, + hub=hub, reverse_proxy_endpoint=reverse_proxy_endpoint ) diff --git a/shared_requirements.txt b/shared_requirements.txt index 1a316c14e1ee..8a69c8434cc5 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -141,6 +141,10 @@ backports.functools-lru-cache >= 1.6.4; python_version == "2.7" #override azure-data-tables msrest>=0.6.21 #override azure-eventhub azure-core<2.0.0,>=1.14.0 #override azure-identity azure-core<2.0.0,>=1.11.0 +#override azure-keyvault-administration azure-core<2.0.0,>=1.15.0 +#override azure-keyvault-certificates azure-core<2.0.0,>=1.15.0 +#override azure-keyvault-keys azure-core<2.0.0,>=1.15.0 +#override azure-keyvault-secrets azure-core<2.0.0,>=1.15.0 #override azure-identity cryptography>=2.5 #override azure-keyvault-administration msrest>=0.6.21 #override azure-keyvault-administration azure-core<2.0.0,>=1.11.0 diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 67535183ba62..ab9d70a3c2e1 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -15,6 +15,7 @@ ) from .keyvault_preparer import KeyVaultPreparer from .powershell_preparer import PowerShellPreparer +from .proxy_docker_startup import start_test_proxy, stop_test_proxy, test_proxy from .proxy_testcase import recorded_by_proxy from .sanitizers import ( add_body_key_sanitizer, @@ -57,6 +58,9 @@ "CachedResourceGroupPreparer", "PowerShellPreparer", "recorded_by_proxy", + "test_proxy", + "start_test_proxy", + "stop_test_proxy", "ResponseCallback", "RetryCounter", "FakeTokenCredential", diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_docker_startup.py b/tools/azure-sdk-tools/devtools_testutils/proxy_docker_startup.py new file mode 100644 index 000000000000..618727596fe8 --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_docker_startup.py @@ -0,0 +1,160 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import json +import os +import logging +import requests +import shlex +import sys +import time +from typing import TYPE_CHECKING + +import pytest +import subprocess + +from .config import PROXY_URL + +if TYPE_CHECKING: + from typing import Optional + + +_LOGGER = logging.getLogger() + +CONTAINER_NAME = "ambitious_azsdk_test_proxy" +LINUX_IMAGE_SOURCE_PREFIX = "azsdkengsys.azurecr.io/engsys/testproxy-lin" +WINDOWS_IMAGE_SOURCE_PREFIX = "azsdkengsys.azurecr.io/engsys/testproxy-win" +CONTAINER_STARTUP_TIMEOUT = 6000 +PROXY_MANUALLY_STARTED = os.getenv('PROXY_MANUAL_START', False) + +REPO_ROOT = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "..")) + + +def get_image_tag(): + # type: () -> str + """Gets the test proxy Docker image tag from the docker-start-proxy.ps1 script in /eng/common""" + pwsh_script_location = os.path.abspath( + os.path.join(REPO_ROOT, os.path.relpath("eng/common/testproxy/docker-start-proxy.ps1")) + ) + + image_tag = None + with open(pwsh_script_location, "r") as f: + for line in f: + if line.startswith("$SELECTED_IMAGE_TAG"): + image_tag_with_quotes = line.split()[-1] + image_tag = image_tag_with_quotes.strip('"') + + return image_tag + + +def get_container_info(): + # type: () -> Optional[dict] + """Returns a dictionary containing the test proxy container's information, or None if the container isn't present""" + proc = subprocess.Popen( + shlex.split("docker container ls -a --format '{{json .}}' --filter name=" + CONTAINER_NAME), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + output, stderr = proc.communicate() + try: + # This will succeed if we found a container with CONTAINER_NAME + return json.loads(output) + # We'll get a JSONDecodeError on Py3 (ValueError on Py2) if output is empty (i.e. there's no proxy container) + except ValueError: + # Didn't find a container with CONTAINER_NAME + return None + + +def create_container(): + # type: () -> None + """Creates the test proxy Docker container""" + # Most of the time, running this script on a Windows machine will work just fine, as Docker defaults to Linux + # containers. However, in CI, Windows images default to _Windows_ containers. We cannot swap them. We can tell + # if we're in a CI build by checking for the environment variable TF_BUILD. + if sys.platform.startswith("win") and os.environ.get("TF_BUILD"): + image_prefix = WINDOWS_IMAGE_SOURCE_PREFIX + path_prefix = "C:" + linux_container_args = "" + else: + image_prefix = LINUX_IMAGE_SOURCE_PREFIX + path_prefix = "" + linux_container_args = "--add-host=host.docker.internal:host-gateway" + + image_tag = get_image_tag() + proc = subprocess.Popen( + shlex.split( + "docker container create -v '{}:{}/etc/testproxy' {} -p 5001:5001 -p 5000:5000 --name {} {}:{}".format( + REPO_ROOT, path_prefix, linux_container_args, CONTAINER_NAME, image_prefix, image_tag + ) + ) + ) + proc.communicate() + + +def start_test_proxy(): + # type: () -> None + """Starts the test proxy and returns when the proxy server is ready to receive requests""" + + if not PROXY_MANUALLY_STARTED: + _LOGGER.info("Starting the test proxy container...") + + container_info = get_container_info() + if container_info: + _LOGGER.debug("Found an existing instance of the test proxy container.") + + if container_info["State"] == "running": + _LOGGER.debug("Proxy container is already running. Exiting...") + return + + else: + _LOGGER.debug("No instance of the test proxy container found. Attempting creation...") + create_container() + + _LOGGER.debug("Attempting to start the test proxy container...") + + proc = subprocess.Popen(shlex.split("docker container start " + CONTAINER_NAME)) + proc.communicate() + + # Wait for the proxy server to become available + start = time.time() + now = time.time() + status_code = 0 + while now - start < CONTAINER_STARTUP_TIMEOUT and status_code != 200: + try: + response = requests.get(PROXY_URL.rstrip("/") + "/Info/Available", timeout=60) + status_code = response.status_code + # We get an SSLError if the container is started but the endpoint isn't available yet + except requests.exceptions.SSLError: + pass + now = time.time() + + +def stop_test_proxy(): + # type: () -> None + """Stops any running instance of the test proxy""" + + if not PROXY_MANUALLY_STARTED: + _LOGGER.info("Stopping the test proxy container...") + + container_info = get_container_info() + if container_info: + if container_info["State"] == "running": + _LOGGER.debug("Found a running instance of the test proxy container; shutting it down...") + + proc = subprocess.Popen(shlex.split("docker container stop " + CONTAINER_NAME)) + proc.communicate() + else: + _LOGGER.debug("No running instance of the test proxy container found. Exiting...") + + +@pytest.fixture(scope="session") +def test_proxy(): + """Pytest fixture to be used before running any tests that are recorded with the test proxy""" + start_test_proxy() + # Everything before this yield will be run before fixtures that invoke this one are run + # Everything after it will be run after invoking fixtures are done executing + yield + stop_test_proxy()