Skip to content

Commit f2036db

Browse files
authored
Add Subscription property to AzureCliCredentialOptions (Azure#47817)
1 parent c2dd7d4 commit f2036db

File tree

7 files changed

+75
-3
lines changed

7 files changed

+75
-3
lines changed

sdk/identity/Azure.Identity/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 1.14.0-beta.1 (Unreleased)
44

55
### Features Added
6+
- Added a `Subscription` property to `AzureCliCredentialOptions` to allow specifying the Azure subscription ID or name to use when authenticating with the Azure CLI.
67

78
### Breaking Changes
89

sdk/identity/Azure.Identity/api/Azure.Identity.net8.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public partial class AzureCliCredentialOptions : Azure.Identity.TokenCredentialO
6565
public AzureCliCredentialOptions() { }
6666
public System.Collections.Generic.IList<string> AdditionallyAllowedTenants { get { throw null; } }
6767
public System.TimeSpan? ProcessTimeout { get { throw null; } set { } }
68+
public string Subscription { get { throw null; } set { } }
6869
public string TenantId { get { throw null; } set { } }
6970
}
7071
public partial class AzureDeveloperCliCredential : Azure.Core.TokenCredential

sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public partial class AzureCliCredentialOptions : Azure.Identity.TokenCredentialO
6363
public AzureCliCredentialOptions() { }
6464
public System.Collections.Generic.IList<string> AdditionallyAllowedTenants { get { throw null; } }
6565
public System.TimeSpan? ProcessTimeout { get { throw null; } set { } }
66+
public string Subscription { get { throw null; } set { } }
6667
public string TenantId { get { throw null; } set { } }
6768
}
6869
public partial class AzureDeveloperCliCredential : Azure.Core.TokenCredential

sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class AzureCliCredential : TokenCredential
4848
private readonly bool _logPII;
4949
private readonly bool _logAccountDetails;
5050
internal string TenantId { get; }
51+
internal string Subscription { get; }
5152
internal string[] AdditionallyAllowedTenantIds { get; }
5253
internal bool _isChainedCredential;
5354
internal TenantIdResolverBase TenantIdResolver { get; }
@@ -75,6 +76,7 @@ internal AzureCliCredential(CredentialPipeline pipeline, IProcessService process
7576
_path = !string.IsNullOrEmpty(EnvironmentVariables.Path) ? EnvironmentVariables.Path : DefaultPath;
7677
_processService = processService ?? ProcessService.Default;
7778
TenantId = Validations.ValidateTenantId(options?.TenantId, $"{nameof(options)}.{nameof(options.TenantId)}", true);
79+
Subscription = options?.Subscription;
7880
TenantIdResolver = options?.TenantIdResolver ?? TenantIdResolverBase.Default;
7981
AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds((options as ISupportsAdditionallyAllowedTenants)?.AdditionallyAllowedTenants);
8082
ProcessTimeout = options?.ProcessTimeout ?? TimeSpan.FromSeconds(13);
@@ -128,7 +130,7 @@ private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, Toke
128130
Validations.ValidateTenantId(tenantId, nameof(context.TenantId), true);
129131
ScopeUtilities.ValidateScope(resource);
130132

131-
GetFileNameAndArguments(resource, tenantId, out string fileName, out string argument);
133+
GetFileNameAndArguments(resource, tenantId, Subscription, out string fileName, out string argument);
132134
ProcessStartInfo processStartInfo = GetAzureCliProcessStartInfo(fileName, argument);
133135
using var processRunner = new ProcessRunner(_processService.Create(processStartInfo), ProcessTimeout, _logPII, cancellationToken);
134136

@@ -209,14 +211,19 @@ private ProcessStartInfo GetAzureCliProcessStartInfo(string fileName, string arg
209211
Environment = { { "PATH", _path } }
210212
};
211213

212-
private static void GetFileNameAndArguments(string resource, string tenantId, out string fileName, out string argument)
214+
private static void GetFileNameAndArguments(string resource, string tenantId, string subscriptionId, out string fileName, out string argument)
213215
{
214216
string command = tenantId switch
215217
{
216218
null => $"az account get-access-token --output json --resource {resource}",
217219
_ => $"az account get-access-token --output json --resource {resource} --tenant {tenantId}"
218220
};
219221

222+
if (!string.IsNullOrEmpty(subscriptionId))
223+
{
224+
command += $" --subscription \"{subscriptionId}\"";
225+
}
226+
220227
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
221228
{
222229
fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");

sdk/identity/Azure.Identity/src/Credentials/AzureCliCredentialOptions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace Azure.Identity
1111
/// </summary>
1212
public class AzureCliCredentialOptions : TokenCredentialOptions, ISupportsAdditionallyAllowedTenants
1313
{
14+
private string _subscription;
15+
1416
/// <summary>
1517
/// The ID of the tenant to which the credential will authenticate by default. If not specified, the credential will authenticate to any requested tenant, and will default to the tenant provided to the 'az login' command.
1618
/// </summary>
@@ -27,5 +29,22 @@ public class AzureCliCredentialOptions : TokenCredentialOptions, ISupportsAdditi
2729
/// The Cli process timeout.
2830
/// </summary>
2931
public TimeSpan? ProcessTimeout { get; set; }
32+
33+
/// <summary>
34+
/// The subscription name or Id to use for authentication. This equates to the --subscription parameter in the Azure CLI.
35+
/// </summary>
36+
public string Subscription
37+
{
38+
get => _subscription;
39+
set
40+
{
41+
if (!Validations.IsValidateSubscriptionNameOrId(value))
42+
{
43+
throw new ArgumentException("The provided subscription contains invalid characters. If this is the name of a subscription, use its ID instead.");
44+
}
45+
46+
_subscription = value;
47+
}
48+
}
3049
}
3150
}

sdk/identity/Azure.Identity/src/Validations.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,27 @@ public static Uri ValidateAuthorityHost(Uri authorityHost)
5757
return authorityHost;
5858
}
5959

60+
public static bool IsValidateSubscriptionNameOrId(string subscription)
61+
{
62+
if (string.IsNullOrEmpty(subscription))
63+
{
64+
return false;
65+
}
66+
foreach (char c in subscription)
67+
{
68+
if (!IsValidateSubscriptionNameOrIdCharacter(c))
69+
{
70+
return false;
71+
}
72+
}
73+
return true;
74+
}
75+
76+
private static bool IsValidateSubscriptionNameOrIdCharacter(char c)
77+
{
78+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == ' ') || (c == '-') || (c == '_');
79+
}
80+
6081
private static bool IsValidTenantCharacter(char c)
6182
{
6283
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '.') || (c == '-');

sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,16 @@ public override TokenCredential GetTokenCredential(CommonCredentialTestConfig co
4545
[Test]
4646
public async Task AuthenticateWithCliCredential(
4747
[Values(null, TenantIdHint)] string tenantId,
48+
[Values(null, "1a7eed92-726e-46c0-b21d-a3db74b3b58c", "My Subscription Name -_")] string subscription,
4849
[Values(true)] bool allowMultiTenantAuthentication,
4950
[Values(null, TenantId)] string explicitTenantId)
5051
{
51-
var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId);
52+
var context = new TokenRequestContext([Scope], tenantId: tenantId);
5253
var options = new AzureCliCredentialOptions { TenantId = explicitTenantId, AdditionallyAllowedTenants = { TenantIdHint } };
54+
if (subscription != null)
55+
{
56+
options.Subscription = subscription;
57+
}
5358
string expectedTenantId = TenantIdResolverBase.Default.Resolve(explicitTenantId, context, TenantIdResolverBase.AllTenants);
5459
var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForAzureCli();
5560

@@ -70,6 +75,23 @@ public async Task AuthenticateWithCliCredential(
7075
{
7176
Assert.That(testProcess.StartInfo.Arguments, Does.Not.Contain("-tenant"));
7277
}
78+
79+
if (subscription != null)
80+
{
81+
Assert.That(testProcess.StartInfo.Arguments, Does.Contain($"--subscription \"{subscription}\""));
82+
}
83+
else
84+
{
85+
Assert.That(testProcess.StartInfo.Arguments, Does.Not.Contain("--subscription"));
86+
}
87+
}
88+
89+
[Test]
90+
public void AzureCliCredentialOptionsValidatesSubscriptionOption()
91+
{
92+
Assert.Throws<ArgumentException>(() => new AzureCliCredentialOptions { Subscription = "My Subscription Name with a quote \"" });
93+
new AzureCliCredentialOptions { Subscription = "My Subscription Name -_" };
94+
new AzureCliCredentialOptions { Subscription = Guid.NewGuid().ToString() };
7395
}
7496

7597
[Test]

0 commit comments

Comments
 (0)