Skip to content

Commit ac1de93

Browse files
committed
Add Azure Login support for connecting to cloud orgs
Fixes #243 --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/igoravl/TfsCmdlets/issues/243?shareId=XXXX-XXXX-XXXX-XXXX).
1 parent 4b27cde commit ac1de93

File tree

8 files changed

+164
-8
lines changed

8 files changed

+164
-8
lines changed

CSharp/TfsCmdlets.SourceGenerators/Generators/Cmdlets/Generator.cs

+25-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,29 @@ namespace TfsCmdlets.SourceGenerators.Generators.Cmdlets
66
public class CmdletGenerator : BaseGenerator<Filter, TypeProcessor>
77
{
88
protected override string GeneratorName => nameof(CmdletGenerator);
9+
10+
protected override void GenerateCmdletParameters(CmdletInfo cmdletInfo)
11+
{
12+
base.GenerateCmdletParameters(cmdletInfo);
13+
14+
if (cmdletInfo.Name.StartsWith("Connect-"))
15+
{
16+
cmdletInfo.Parameters.Add(new CmdletParameter
17+
{
18+
Name = "AzCli",
19+
Type = "SwitchParameter",
20+
Mandatory = false,
21+
Position = -1
22+
});
23+
24+
cmdletInfo.Parameters.Add(new CmdletParameter
25+
{
26+
Name = "UseMSI",
27+
Type = "SwitchParameter",
28+
Mandatory = false,
29+
Position = -1
30+
});
31+
}
32+
}
933
}
10-
}
34+
}

CSharp/TfsCmdlets/Cmdlets/Credential/NewCredential.cs

+42-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Management.Automation;
1+
using System.Management.Automation;
22
using Microsoft.VisualStudio.Services.Common;
33
using System.Net;
44
using Microsoft.VisualStudio.Services.Client;
@@ -19,6 +19,18 @@ partial class NewCredential
1919
/// </summary>
2020
[Parameter(Position = 0, Mandatory = true)]
2121
public Uri Url { get; set; }
22+
23+
/// <summary>
24+
/// Specifies that the credentials should be obtained from the currently logged in Azure CLI user.
25+
/// </summary>
26+
[Parameter(Mandatory = false)]
27+
public SwitchParameter AzCli { get; set; }
28+
29+
/// <summary>
30+
/// Specifies that the credentials should be obtained from the Azure Managed Identity present in the current script context.
31+
/// </summary>
32+
[Parameter(Mandatory = false)]
33+
public SwitchParameter UseMSI { get; set; }
2234
}
2335

2436
[CmdletController(typeof(VssCredentials), CustomCmdletName = "NewCredential")]
@@ -27,6 +39,9 @@ partial class GetCredentialController
2739
[Import]
2840
private IInteractiveAuthentication InteractiveAuthentication { get; }
2941

42+
[Import]
43+
private IAzCliAuthentication AzCliAuthentication { get; }
44+
3045
protected override IEnumerable Run()
3146
{
3247
var connectionMode = ConnectionMode.CachedCredentials;
@@ -39,6 +54,10 @@ protected override IEnumerable Run()
3954
connectionMode = ConnectionMode.AccessToken;
4055
else if (Interactive)
4156
connectionMode = ConnectionMode.Interactive;
57+
else if (AzCli)
58+
connectionMode = ConnectionMode.AzCli;
59+
else if (UseMSI)
60+
connectionMode = ConnectionMode.UseMSI;
4261

4362
NetworkCredential netCred = null;
4463

@@ -124,6 +143,24 @@ protected override IEnumerable Run()
124143
throw new Exception("Interactive authentication is not supported for TFS / Azure DevOps Server in PowerShell Core. Please use either a username/password credential or a Personal Access Token.");
125144
}
126145

146+
case ConnectionMode.AzCli:
147+
{
148+
Logger.Log("Using Azure CLI credential");
149+
150+
yield return new VssCredentials(
151+
new VssOAuthAccessTokenCredential(AzCliAuthentication.GetToken(Url)));
152+
break;
153+
}
154+
155+
case ConnectionMode.UseMSI:
156+
{
157+
Logger.Log("Using Managed Identity credential");
158+
159+
yield return new VssCredentials(
160+
new VssOAuthAccessTokenCredential(AzCliAuthentication.GetToken(Url, useMsi: true)));
161+
break;
162+
}
163+
127164
default:
128165
{
129166
throw new Exception($"Invalid parameter set '{connectionMode}'");
@@ -150,7 +187,9 @@ private enum ConnectionMode
150187
CredentialObject,
151188
UserNamePassword,
152189
AccessToken,
153-
Interactive
190+
Interactive,
191+
AzCli,
192+
UseMSI
154193
}
155194
}
156-
}
195+
}

CSharp/TfsCmdlets/Cmdlets/Organization/ConnectOrganization.cs

+13-1
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,17 @@ partial class ConnectOrganization
4747
[Alias("Collection")]
4848
[ValidateNotNull]
4949
public object Organization { get; set; }
50+
51+
/// <summary>
52+
/// Specifies that the credentials should be obtained from the currently logged in Azure CLI user.
53+
/// </summary>
54+
[Parameter(Mandatory = false)]
55+
public SwitchParameter AzCli { get; set; }
56+
57+
/// <summary>
58+
/// Specifies that the credentials should be obtained from the Azure Managed Identity present in the current script context.
59+
/// </summary>
60+
[Parameter(Mandatory = false)]
61+
public SwitchParameter UseMSI { get; set; }
5062
}
51-
}
63+
}

CSharp/TfsCmdlets/Cmdlets/Team/ConnectTeam.cs

+13-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ partial class ConnectTeam
1717
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
1818
[ValidateNotNull()]
1919
public object Team { get; set; }
20+
21+
/// <summary>
22+
/// Specifies that the credentials should be obtained from the currently logged in Azure CLI user.
23+
/// </summary>
24+
[Parameter(Mandatory = false)]
25+
public SwitchParameter AzCli { get; set; }
26+
27+
/// <summary>
28+
/// Specifies that the credentials should be obtained from the Azure Managed Identity present in the current script context.
29+
/// </summary>
30+
[Parameter(Mandatory = false)]
31+
public SwitchParameter UseMSI { get; set; }
2032
}
2133

2234
[CmdletController(typeof(Models.Team))]
@@ -36,4 +48,4 @@ protected override IEnumerable Run()
3648
yield return Team;
3749
}
3850
}
39-
}
51+
}

CSharp/TfsCmdlets/Cmdlets/TeamProject/ConnectTeamProject.cs

+13-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ partial class ConnectTeamProject
1515
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
1616
[ValidateNotNull()]
1717
public object Project { get; set; }
18+
19+
/// <summary>
20+
/// Specifies that the credentials should be obtained from the currently logged in Azure CLI user.
21+
/// </summary>
22+
[Parameter(Mandatory = false)]
23+
public SwitchParameter AzCli { get; set; }
24+
25+
/// <summary>
26+
/// Specifies that the credentials should be obtained from the Azure Managed Identity present in the current script context.
27+
/// </summary>
28+
[Parameter(Mandatory = false)]
29+
public SwitchParameter UseMSI { get; set; }
1830
}
1931

2032
[CmdletController(typeof(WebApiTeamProject))]
@@ -35,4 +47,4 @@ protected override IEnumerable Run()
3547
[Import]
3648
private ICurrentConnections CurrentConnections { get; }
3749
}
38-
}
50+
}

CSharp/TfsCmdlets/Cmdlets/TeamProjectCollection/ConnectTeamProjectCollection.cs

+16-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ partial class ConnectTeamProjectCollection
4949
[Alias("Organization")]
5050
[ValidateNotNull]
5151
public object Collection { get; set; }
52+
53+
/// <summary>
54+
/// Specifies that the credentials should be obtained from the currently logged in Azure CLI user.
55+
/// </summary>
56+
[Parameter(Mandatory = false)]
57+
public SwitchParameter AzCli { get; set; }
58+
59+
/// <summary>
60+
/// Specifies that the credentials should be obtained from the Azure Managed Identity present in the current script context.
61+
/// </summary>
62+
[Parameter(Mandatory = false)]
63+
public SwitchParameter UseMSI { get; set; }
5264
}
5365

5466
[CmdletController(typeof(Connection))]
@@ -57,6 +69,9 @@ partial class ConnectTeamProjectCollectionController
5769
[Import]
5870
private ICurrentConnections CurrentConnections { get; }
5971

72+
[Import]
73+
private IAzCliAuthentication AzCliAuthentication { get; }
74+
6075
protected override IEnumerable Run()
6176
{
6277
var tpc = Data.GetCollection(new { Collection = Collection ?? Parameters.Get<object>("Organization") });
@@ -75,4 +90,4 @@ protected override IEnumerable Run()
7590
yield return tpc;
7691
}
7792
}
78-
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace TfsCmdlets.Services
2+
{
3+
public interface IAzCliAuthentication
4+
{
5+
string GetToken(Uri uri, bool useMsi = false);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Azure.Core;
4+
using Azure.Identity;
5+
6+
namespace TfsCmdlets.Services.Impl
7+
{
8+
[Export(typeof(IAzCliAuthentication)), Shared]
9+
public class AzCliAuthenticationImpl : IAzCliAuthentication
10+
{
11+
private const string AzureDevOpsResourceId = "499b84ac-1321-427f-aa17-267ca6975798";
12+
13+
public string GetToken(Uri uri, bool useMsi = false)
14+
{
15+
var token = useMsi ? GetMsiTokenAsync().Result : GetAzCliTokenAsync().Result;
16+
return token;
17+
}
18+
19+
private async Task<string> GetAzCliTokenAsync()
20+
{
21+
var credential = new DefaultAzureCredential();
22+
var tokenRequestContext = new TokenRequestContext(new[] { $"{AzureDevOpsResourceId}/.default" });
23+
var token = await credential.GetTokenAsync(tokenRequestContext);
24+
return token.Token;
25+
}
26+
27+
private async Task<string> GetMsiTokenAsync()
28+
{
29+
var credential = new ManagedIdentityCredential();
30+
var tokenRequestContext = new TokenRequestContext(new[] { $"{AzureDevOpsResourceId}/.default" });
31+
var token = await credential.GetTokenAsync(tokenRequestContext);
32+
return token.Token;
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)