Skip to content

Commit 16b4a4f

Browse files
authored
Add interactive logon for PowerShell Core (#169)
* Add MSAL packages * Add interactive auth support for PSCore * Update release notes +semver: minor * Update client ID * Refactor code for readability * Update release notes * Add Debug config as task default * Change parameter to Enum * Fix typo * Add new aliases * Improve folder handling * Add preliminary support for sourcegen * Add test * Fix test * Update tests * Add validation against empty strings * Improve path handling * Replace string values with Enum * Update release notes * Replace Path to prevent sep char from being replaced * Fix item path handling * Update tests * Remove redundant call to Data service * Fix bug in recursive folder creation * Update release notes
1 parent 2256049 commit 16b4a4f

28 files changed

+452
-101
lines changed

.vscode/tasks.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
"-NoProfile",
5151
"-File",
5252
"${workspaceRoot}/build.ps1",
53+
"-Configuration",
54+
"Debug",
5355
"-Targets",
5456
"Rebuild",
5557
"-SkipTests"

CSharp/TfsCmdlets.Common/Attributes.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ public CmdletControllerAttribute(Type dataType) : base(typeof(IController))
2323
}
2424
}
2525

26+
[AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
27+
public class ModelAttribute: Attribute
28+
{
29+
public Type DataType { get; }
30+
31+
public ModelAttribute(Type dataType)
32+
{
33+
DataType = dataType;
34+
}
35+
}
36+
2637
// [AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
2738
// public sealed class DesktopOnlyAttribute : Attribute
2839
// {

CSharp/TfsCmdlets.Common/Enums.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,25 @@ public enum WorkItemLinkType
205205
ArtifactLink = 1 << 31,
206206
}
207207

208+
/// <summary>
209+
/// Work Item Query Item Scope
210+
/// </summary>
211+
[Flags]
212+
public enum QueryItemScope
213+
{
214+
/// <summary>
215+
/// Personal Scope ("My Queries" folder)
216+
/// </summary>
217+
Personal = 1,
218+
219+
/// <summary>
220+
/// Shared Scope ("Shared Queries" folder)
221+
/// </summary>
222+
Shared = 2,
223+
224+
/// <summary>
225+
/// Both scopes (all folders)
226+
/// </summary>
227+
Both = 3
228+
}
208229
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
2+
3+
namespace TfsCmdlets.Models.WorkItem.Query
4+
{
5+
[Model(typeof(QueryHierarchyItem))]
6+
partial class QueryItem
7+
{
8+
9+
}
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace TfsCmdlets.Services
2+
{
3+
public interface IInteractiveAuthentication
4+
{
5+
string GetToken(Uri uri);
6+
}
7+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using Microsoft.Identity.Client;
2+
using Microsoft.Identity.Client.Desktop;
3+
using System.Reflection;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace TfsCmdlets.Services.Impl
8+
{
9+
[Export(typeof(IInteractiveAuthentication)), Shared]
10+
public class InteractiveAuthenticationImpl : IInteractiveAuthentication
11+
{
12+
private const string CLIENT_ID = "9f44d9a2-86ef-4794-b2b2-f9038a2628e0";
13+
private const string SCOPE_ID = "499b84ac-1321-427f-aa17-267ca6975798/user_impersonation";
14+
15+
public string GetToken(Uri uri)
16+
{
17+
var authResult = SignInUserAndGetTokenUsingMSAL(new[] { SCOPE_ID }).GetAwaiter().GetResult();
18+
var authHeader = authResult.AccessToken;
19+
20+
return authHeader;
21+
}
22+
23+
/// <summary>
24+
/// Sign-in user using MSAL and obtain an access token for Azure DevOps
25+
/// </summary>
26+
/// <param name="scopes"></param>
27+
/// <returns>AuthenticationResult</returns>
28+
private static async Task<AuthenticationResult> SignInUserAndGetTokenUsingMSAL(string[] scopes)
29+
{
30+
var application = PublicClientApplicationBuilder
31+
.CreateWithApplicationOptions(new PublicClientApplicationOptions
32+
{
33+
AadAuthorityAudience = AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount,
34+
ClientId = CLIENT_ID,
35+
ClientName = "TfsCmdlets.InteractiveAuth",
36+
ClientVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString()
37+
})
38+
.WithDesktopFeatures()
39+
.WithDefaultRedirectUri()
40+
.Build();
41+
42+
try
43+
{
44+
var accounts = (await application.GetAccountsAsync()).ToList();
45+
46+
return await application
47+
.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
48+
.ExecuteAsync();
49+
}
50+
catch (MsalUiRequiredException ex)
51+
{
52+
var cts = new CancellationTokenSource();
53+
cts.CancelAfter(60000);
54+
55+
return await application
56+
.AcquireTokenInteractive(scopes)
57+
.WithPrompt(Prompt.SelectAccount)
58+
.WithClaims(ex.Claims)
59+
.ExecuteAsync(cts.Token);
60+
}
61+
}
62+
}
63+
}

CSharp/TfsCmdlets.Common/TfsCmdlets.Common.csproj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
<CompilerGeneratedFilesOutputPath>obj/Generated</CompilerGeneratedFilesOutputPath>
1616
</PropertyGroup>
1717

18+
<ItemGroup>
19+
<!-- Source Generators -->
20+
<ProjectReference Include="../TfsCmdlets.SourceGenerators/TfsCmdlets.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
21+
</ItemGroup>
22+
1823
<ItemGroup>
1924
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="16.*-*" />
2025
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="16.*-*" />
@@ -28,6 +33,8 @@
2833
<PackageReference Include="System.Composition" Version="5.0.1" />
2934
<PackageReference Include="System.Composition.Hosting" Version="5.0.1" />
3035
<PackageReference Include="Microsoft.Win32.Registry" Version="5.*-*" />
36+
<PackageReference Include="Microsoft.Identity.Client" Version="4.*" />
37+
<PackageReference Include="Microsoft.Identity.Client.Desktop" Version="4.*" />
3138
</ItemGroup>
3239

3340
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
@@ -39,6 +46,12 @@
3946
<Compile Remove="**/*-core.cs" />
4047
</ItemGroup>
4148

49+
<ItemGroup>
50+
<Compile Remove="Generated\**" />
51+
<EmbeddedResource Remove="Generated\**" />
52+
<None Remove="Generated\**" />
53+
</ItemGroup>
54+
4255
<ItemGroup>
4356
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
4457
<_Parameter1>TfsCmdlets</_Parameter1>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ namespace TfsCmdlets.SourceGenerators.Generators.Cmdlets
55
[Generator]
66
public class CmdletGenerator : BaseGenerator<Filter, TypeProcessor>
77
{
8-
protected override string GeneratorName => "CmdletGenerator";
8+
protected override string GeneratorName => nameof(CmdletGenerator);
99
}
1010
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace TfsCmdlets.SourceGenerators.Generators.Controllers
44
{
55
[Generator]
6-
public class CmdletGenerator : BaseGenerator<Filter, TypeProcessor>
6+
public class ControllerGenerator : BaseGenerator<Filter, TypeProcessor>
77
{
8-
protected override string GeneratorName => "ControllerGenerator";
8+
protected override string GeneratorName => nameof(ControllerGenerator);
99
}
1010
}

CSharp/TfsCmdlets.SourceGenerators/Generators/Models/Filter.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ namespace TfsCmdlets.SourceGenerators.Generators.Models
44
{
55
public class Filter : BaseFilter
66
{
7-
public override bool ShouldProcessType(INamedTypeSymbol type)
8-
{
9-
var baseClass = type.BaseType;
10-
11-
return baseClass != null && baseClass.FullName().StartsWith("TfsCmdlets.Models.ModelBase");
12-
}
7+
public override bool ShouldProcessType(INamedTypeSymbol type) => type.HasAttribute("ModelAttribute");
138
}
149
}

0 commit comments

Comments
 (0)