Conversation
ARM TypeSpec specs declare OAuth2 with relative scopes like
'user_impersonation'. Passing this bare scope to the Azure
identity libraries fails because it resolves to the wrong
resource (Microsoft Graph instead of Azure Management).
When scopes are relative (not absolute URLs), generate code
that derives the scope from the endpoint origin at runtime
using the '{endpoint_origin}/.default' pattern. This is
consistent with other Azure SDK languages (Python, Go) and
correctly handles sovereign clouds.
Absolute scopes (e.g. 'https://vault.azure.net/.default')
are still emitted as static strings.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates the Rust TypeSpec emitter to correctly derive Azure Resource Manager (ARM) OAuth2 scopes from the service endpoint when specs provide relative scopes (e.g., user_impersonation), preventing tokens from being requested for the wrong resource.
Changes:
- Emit
{endpoint_origin}/.defaultat runtime for relative scopes when constructingBearerTokenAuthorizationPolicy. - Keep absolute (URL) scopes emitted as static strings.
- Regenerate affected spector test crates to reflect the new scope behavior.
Reviewed changes
Copilot reviewed 1 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/typespec-rust/src/codegen/clients.ts | Adds relative-scope detection and emits runtime-derived /.default scope based on endpoint origin. |
| packages/typespec-rust/test/spector/azure/resource-manager/resources/src/generated/clients/resources_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/operation-templates/src/generated/clients/operation_templates_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/non-resource/src/generated/clients/non_resource_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/multi-service/src/generated/clients/combined_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/multi-service-shared-models/src/generated/clients/combined_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/multi-service-older-versions/src/generated/clients/combined_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/method-subscription-id/src/generated/clients/method_subscription_id_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/large-header/src/generated/clients/large_header_client.rs | Updates generated auth policy scopes to use derived /.default. |
| packages/typespec-rust/test/spector/azure/resource-manager/common-properties/src/generated/clients/common_properties_client.rs | Updates generated auth policy scopes to use derived /.default. |
Comments suppressed due to low confidence (1)
packages/typespec-rust/src/codegen/clients.ts:700
- If
arcTokenCred.scopesever contains a mix of relative and absolute scopes, this branch drops all provided scopes and emits only the derived/.defaultscope. A more robust approach is to transform each scope independently (relative → derived runtime/.default, absolute → keep as-is) so mixed inputs don’t silently lose information.
const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http'));
if (hasRelativeScopes) {
// Relative scopes (e.g. "user_impersonation" from ARM specs) must be
// qualified at runtime using the service endpoint so that sovereign
// clouds work correctly. The standard Azure SDK pattern is
// "{endpoint_origin}/.default".
return `let auth_policy: Arc<dyn Policy> = Arc::new(BearerTokenAuthorizationPolicy::new(credential, vec![format!("{}/.default", ${endpointParamName}.origin().ascii_serialization())]));`;
}
const scopes = new Array<string>();
for (const scope of arcTokenCred.scopes) {
scopes.push(`"${scope}"`);
}
return `let auth_policy: Arc<dyn Policy> = Arc::new(BearerTokenAuthorizationPolicy::new(credential, vec![${scopes.join(', ')}]));`;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const arcTokenCred = utils.asTypeOf<rust.TokenCredential>(param.type, 'tokenCredential', 'arc'); | ||
| if (arcTokenCred) { | ||
| use.add('azure_core::http::policies', 'auth::BearerTokenAuthorizationPolicy', 'Policy'); | ||
| const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http')); |
There was a problem hiding this comment.
!s.startsWith('http') misclassifies non-HTTP absolute scopes (e.g., api://{client-id}/.default) as “relative”, which would incorrectly replace them with {endpoint_origin}/.default. Consider detecting “absolute” scopes by checking for a URI scheme (e.g., s.includes('://')) or using URL parsing where possible, and only treating truly relative bare scopes as relative.
| const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http')); | |
| const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.includes('://')); |
| const hasRelativeScopes = arcTokenCred.scopes.some(s => !s.startsWith('http')); | ||
| if (hasRelativeScopes) { |
There was a problem hiding this comment.
This introduces new branching behavior for scope classification. Add an emitter test that covers (1) relative scopes (user_impersonation), (2) absolute https scopes (https://vault.azure.net/.default), and (3) non-HTTP absolute scopes (e.g., api://.../.default) to prevent regressions in the relative/absolute detection.
There was a problem hiding this comment.
@copilot apply changes based on active feedback
|
@antkmsft @jhendrixMSFT the code changes look fine to me, but I don't know enough about ARM auth scopes. Can you review and approve? |
Problem
ARM TypeSpec specs declare OAuth2 with relative scopes like
user_impersonation. The emitter passes this bare scope verbatim toBearerTokenAuthorizationPolicy::new(). When Azure CLI or other identity libraries try to acquire a token for justuser_impersonation, it resolves to Microsoft Graph (00000003-0000-0000-c000-000000000000) instead of Azure Management, causing authentication failures:Fix
When scopes are relative (not absolute URLs), generate code that derives the scope from the endpoint origin at runtime using the standard Azure SDK pattern:
{endpoint_origin}/.default.Before (broken):
After (fixed):
Absolute scopes (e.g.
https://vault.azure.net/.defaultused by data-plane services) are still emitted as static strings.This is consistent with other Azure SDK languages (Python uses
f"{base_url}/.default", Go usescloud.Services[ResourceManager].Audience + "/.default").Validation