Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Core] Add workaround for cross-tenant authentication with Track 2 SDKs #16797

Merged
merged 4 commits into from
Apr 9, 2021

Conversation

jiasli
Copy link
Member

@jiasli jiasli commented Feb 4, 2021

Fix #16691: az vnet peering create no longer works cross-tenant

⚠ This is only a temporary workaround for Azure/azure-sdk-for-python#8313.

Changes

Manually add external tokens to x-ms-authorization-auxiliary header.

Regarding auto-refresh

As far as I know and also tested for asynchronous Azure operations (long-running operations)

az network vnet peering create 
    --name myVnetAToMyVnetB
    --resource-group myResourceGroupA
    --vnet-name myVnetA 
    --remote-vnet /subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourceGroups/myResourceGroupB/providers/Microsoft.Network/VirtualNetworks/myVnetB 
    --allow-vnet-access 
    --debug

x-ms-authorization-auxiliary is only necessary in the first request:

azure.core.pipeline.policies._universal: Request URL: 'https://management.azure.com/subscriptions/414af076-009b-4282-9a0a-acf75bcb037e/resourceGroups/myResourceGroupA/providers/Microsoft.Network/virtualNetworks/myVnetA/virtualNetworkPeerings/myVnetAToMyVnetB?api-version=2020-07-01'
azure.core.pipeline.policies._universal: Request method: 'PUT'
azure.core.pipeline.policies._universal: Request headers:
azure.core.pipeline.policies._universal:     'x-ms-authorization-auxiliary': 'Bearer **********'
azure.core.pipeline.policies._universal:     'Authorization': '*****'
azure.core.pipeline.policies._universal: Request body:
azure.core.pipeline.policies._universal: ... "remoteVirtualNetwork": {"id": "/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourceGroups/myResourceGroupB/providers/Microsoft.Network/VirtualNetworks/myVnetB"} ...

azure.core.pipeline.policies._universal: Response status: 200
azure.core.pipeline.policies._universal: Response headers:
azure.core.pipeline.policies._universal:     'Azure-AsyncOperation': 'https://management.azure.com/subscriptions/414af076-009b-4282-9a0a-acf75bcb037e/providers/Microsoft.Network/locations/eastus/operations/78792a4b-d45f-4037-8149-18baa4fbff5e?api-version=2020-07-01'

The response is a 200 with Azure-AsyncOperation, which contradicts Track asynchronous Azure operations. (Azure/azure-rest-api-specs#12828)

The following pooling request won't need x-ms-authorization-auxiliary and can also succeed:

GET https://management.azure.com/subscriptions/<subId>/providers/Microsoft.Network/locations/eastus/operations/7de4e22f-7876-4596-9857-bcee50d703da?api-version=2020-07-01
Authorization: Bearer *****

200
{"status": "Succeeded"}

Therefore, we can believe tokens in x-ms-authorization-auxiliary is only used once, so auto-refresh within a long-running operation won't be necessary.

Alternative solutions

Develop a custom policy ExternalBearerTokenCredentialPolicy and add it to the client. However, adding a custom policy to a client is current impracticable (Azure/azure-sdk-for-python#16519).

@yonzhan
Copy link
Collaborator

yonzhan commented Feb 4, 2021

Core

@yonzhan yonzhan added this to the S183 milestone Feb 4, 2021
if external_tenant_tokens:
# Hard-code scheme to 'Bearer' as _BearerTokenCredentialPolicyBase._update_headers does.
client_kwargs['headers']['x-ms-authorization-auxiliary'] = \
', '.join("Bearer {}".format(t[1]) for t in external_tenant_tokens)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can both , and ; be used as the delimiter for each token?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simply don't know. We need to do some test.

The exiting code

aux_tokens = ';'.join(['{} {}'.format(scheme2, tokens2) for scheme2, tokens2, _ in external_tenant_tokens])

contradicts the document Authenticate requests across tenants

Bearer <auxiliary-token1>, EncryptedBearer <auxiliary-token2>, Bearer <auxiliary-token3>

That's why this PR is still marked as a draft. 😉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to my experiment of ARM deployment, the correct delimiter should be , .
When the delimiter is ;, the service returns the following error:

{"error":{"code":"InvalidAuxiliaryTokens","message":"Authentication failed for auxiliary token: 'The auxiliary tokens should have proper format \"'scheme' 'value'\". The '1' auxiliary token(s) were not found in proper format. Invalid tokens are in the response header.'"}}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True! Also verified with:

> az rest --url "https://management.azure.com/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourcegroups?api-version=2020-10-01" --headers "x-ms-authorization-auxiliary=Bearer token1; Bearer token2" --verbose
Request URL: 'https://management.azure.com/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourcegroups?api-version=2020-10-01'
Request method: 'GET'
Request headers:
    'Authorization': 'Bearer eyJ0eXAiOiJKV...'
    'x-ms-authorization-auxiliary': 'Bearer token1; Bearer token2'    
    ...

Response status: 401
Response headers:
    'WWW-Authenticate': 'Bearer authorization_uri="https://login.windows.net/54826b22-38d6-4fb2-bad9-b7b93a3e9c5a", error="invalid_token", error_description="Invalid auxiliary tokens \'Bearer token1; Bearer token2\'."'
    ...
Response content:
{"error":{"code":"InvalidAuxiliaryTokens","message":"Authentication failed for auxiliary token: 'The auxiliary tokens should have proper format \"'scheme' 'value'\". The '1' auxiliary token(s) were not found in proper format. Invalid tokens are in the response header.'"}}
Unauthorized({"error":{"code":"InvalidAuxiliaryTokens","message":"Authentication failed for auxiliary token: 'The auxiliary tokens should have proper format \"'scheme' 'value'\". The '1' auxiliary token(s) were not found in proper format. Invalid tokens are in the response header.'"}})

This means the doc is right and the current CLI code is wrong.

@yonzhan yonzhan modified the milestones: S183 - For Ignite, S184 Feb 26, 2021
@jiasli jiasli marked this pull request as ready for review March 1, 2021 03:05
@jiasli
Copy link
Member Author

jiasli commented Mar 1, 2021

@jsntcy @msyyc, please kindly test for Network module and add some LiveScenarioTests.

I have already written some testing script: #16691 (comment)

You may refer to VMCrossTenantUpdateScenarioTest to write similar tests:

class VMCrossTenantUpdateScenarioTest(LiveScenarioTest):
@ResourceGroupPreparer(name_prefix='cli_test_vm_cross_tenant_', location='westus2')
def test_vm_cross_tenant_update(self, resource_group):
self.kwargs.update({
'location': 'westus2',
'another_rg': self.create_random_name('cli_test_vm_cross_tenant_', 40),
'another_vm': self.create_random_name('cli_test_vm_cross_tenant_', 40),
'image_name': self.create_random_name('cli_test_vm_cross_tenant_', 40),
'aux_sub': '1c638cf4-608f-4ee6-b680-c329e824c3a8',
'aux_tenant': '72f988bf-86f1-41af-91ab-2d7cd011db47',
'vm': self.create_random_name('cli_test_vm_cross_tenant_', 40)
})
# Prepare sig in another tenant
self.cmd('group create -g {another_rg} --location {location} --subscription {aux_sub}',
checks=self.check('name', self.kwargs['another_rg']))
self.cmd(
'vm create -g {another_rg} -n {vm} --image ubuntults --admin-username clitest1 --generate-ssh-key --subscription {aux_sub}')
self.cmd(
'vm run-command invoke -g {another_rg} -n {vm} --command-id RunShellScript --scripts "echo \'sudo waagent -deprovision+user --force\' | at -M now + 1 minutes" --subscription {aux_sub}')
time.sleep(70)
self.cmd('vm deallocate -g {another_rg} -n {vm} --subscription {aux_sub}')
self.cmd('vm generalize -g {another_rg} -n {vm} --subscription {aux_sub}')
res = self.cmd(
'image create -g {another_rg} -n {image_name} --source {vm} --subscription {aux_sub}').get_output_in_json()
self.kwargs.update({
'image_id': res['id']
})
self.cmd(
'vm create -g {rg} -n {vm} --image {image_id} --admin-username clitest1 --generate-ssh-key --nsg-rule NONE')
self.cmd('vm update -g {rg} -n {vm} --set tags.tagName=tagValue', checks=[
self.check('tags.tagName', 'tagValue')
])
self.cmd('group delete -n {another_rg} -y --subscription {aux_sub}')

resource = cmd.cli_ctx.cloud.endpoints.active_directory_resource_id
cred, _, _ = profile.get_login_credentials(resource=resource,
aux_subscriptions=aux_subscriptions)
_, _, _, external_tokens = cred.get_all_tokens('https://management.azure.com/.default')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By moving this to core, it now works for sovereign cloud (solves #15750 (comment)).

@yonzhan yonzhan modified the milestones: S184, S185 Mar 20, 2021
@@ -573,6 +574,10 @@ def get_login_credentials(self, resource=None, subscription_id=None, aux_subscri
if sub[_TENANT_ID] != account[_TENANT_ID]:
external_tenants_info.append(sub[_TENANT_ID])

if external_tenants_info and (identity_type or in_cloud_console()):
raise CLIError("Cross-tenant authentication is not supported by managed identity and Cloud Shell. "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLIError is deprecated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is difficult to classify.

@@ -35,7 +35,7 @@ def _get_token(self, sdk_resource=None):
"""
external_tenant_tokens = None
try:
scheme, token, full_token = self._token_retriever(sdk_resource)
scheme, token, token_entry = self._token_retriever(sdk_resource)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it just a rename operation? Does the object structure change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. No.

# As a temporary workaround, manually add external tokens to 'x-ms-authorization-auxiliary' header.
# https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/authenticate-multi-tenant
if getattr(cred, "_external_tenant_token_retriever", None):
_, _, _, external_tenant_tokens = cred.get_all_tokens(*resource_to_scopes(resource))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is *

Copy link
Member Author

@jiasli jiasli Apr 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_, _, _, external_tenant_tokens = cred.get_all_tokens(*resource_to_scopes(resource))
# Hard-code scheme to 'Bearer' as _BearerTokenCredentialPolicyBase._update_headers does.
client_kwargs['headers']['x-ms-authorization-auxiliary'] = \
', '.join("Bearer {}".format(t[1]) for t in external_tenant_tokens)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a pipeline support?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make a new policy and add it to the pipeline in the future, utilizing Azure/azure-sdk-for-python#17340.

@@ -3363,8 +3345,7 @@ def create_image_version(cmd, resource_group_name, gallery_name, gallery_image_n
gallery_name=gallery_name,
gallery_image_name=gallery_image_name,
gallery_image_version_name=gallery_image_version,
gallery_image_version=image_version,
headers={'x-ms-authorization-auxiliary': external_bearer_token}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You deleted lots of code in vm/custom.py and didn't add anything. How are external tokens handled now? Is another PR required to fix it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You deleted lots of code in vm/custom.py and didn't add anything.

The logic is not deleted, but moved to _get_mgmt_service_client.

How are external tokens handled now?

External tokens are handled automatically when aux_subscriptions is provided to _get_mgmt_service_client.

client = _compute_client_factory(cmd.cli_ctx, aux_subscriptions=aux_subscriptions)

Is another PR required to fix it?

It already works. No other PR needed.

@qwordy
Copy link
Member

qwordy commented Apr 6, 2021

Have you run live test for sig commands?

@jiasli
Copy link
Member Author

jiasli commented Apr 6, 2021

Have you run live test for sig commands?

No. You may take some time to run it.

@qwordy
Copy link
Member

qwordy commented Apr 7, 2021

@jiasli
Copy link
Member Author

jiasli commented Apr 7, 2021

Testing results of VM. Fail. Subscription is not found. Investigating.

It is because the test relies on Microsoft tenant

'aux_tenant': '72f988bf-86f1-41af-91ab-2d7cd011db47',

but your test account doesn't have permission there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

az vnet peering create no longer works cross-tenant
7 participants