Skip to content

Commit 81b36e6

Browse files
committed
Update application to support secretless auth
Update the SeQuester C# application to support secretless authentication.
1 parent 3a4c912 commit 81b36e6

File tree

5 files changed

+37
-6
lines changed

5 files changed

+37
-6
lines changed

actions/sequester/ImportIssues/Program.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,21 @@ private static async Task<QuestGitHubService> CreateService(ImportOptions option
104104
{
105105
Console.WriteLine("Warning: Imported work items won't be assigned based on GitHub assignee.");
106106
}
107+
bool useBearerToken = (options.ApiKeys.QuestAccessToken is not null);
108+
string? token = useBearerToken ?
109+
options.ApiKeys.QuestAccessToken :
110+
options.ApiKeys.QuestKey;
111+
112+
if (string.IsNullOrWhiteSpace(token))
113+
{
114+
throw new InvalidOperationException("Azure DevOps token is missing.");
115+
}
107116

108117
return new QuestGitHubService(
109118
gitHubClient,
110119
ospoClient,
111-
options.ApiKeys.QuestKey,
120+
token,
121+
useBearerToken,
112122
options.AzureDevOps.Org,
113123
options.AzureDevOps.Project,
114124
options.AzureDevOps.AreaPath,

actions/sequester/Quest2GitHub/AzDoClientServices/QuestClient.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Polly;
1+
using System.Net.Http;
2+
using Polly;
23
using Polly.Contrib.WaitAndRetry;
34
using Polly.Retry;
45

@@ -35,14 +36,16 @@ public sealed class QuestClient : IDisposable
3536
/// <param name="token">The personal access token</param>
3637
/// <param name="org">The Azure DevOps organization</param>
3738
/// <param name="project">The Azure DevOps project</param>
38-
public QuestClient(string token, string org, string project)
39+
/// <param name="useBearerToken">True to use a just in time bearer token, false assumes PAT</param>
40+
public QuestClient(string token, string org, string project, bool useBearerToken)
3941
{
4042
QuestOrg = org;
4143
QuestProject = project;
4244
_client = new HttpClient();
4345
_client.DefaultRequestHeaders.Accept.Add(
4446
new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
45-
_client.DefaultRequestHeaders.Authorization =
47+
_client.DefaultRequestHeaders.Authorization = useBearerToken ?
48+
new AuthenticationHeaderValue("Bearer", token) :
4649
new AuthenticationHeaderValue("Basic",
4750
Convert.ToBase64String(Encoding.ASCII.GetBytes($":{token}")));
4851

actions/sequester/Quest2GitHub/Options/ApiKeys.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ public sealed record class ApiKeys
3636
/// </remarks>
3737
public string? AzureAccessToken { get; init; }
3838

39+
/// <summary>
40+
/// The client ID for identifying this app with AzureDevOps.
41+
/// </summary>
42+
/// <remarks>
43+
/// Assign this from an environment variable with the following key, <c>ImportOptions__ApiKeys__AzureAccessToken</c>:
44+
/// <code>
45+
/// env:
46+
/// ImportOptions__ApiKeys__QuestAccessToken: ${{ secrets.QUEST_ACCESS_TOKEN }}
47+
/// </code>
48+
/// </remarks>
49+
public string? QuestAccessToken { get; init; }
50+
3951
/// <summary>
4052
/// The Azure DevOps API key.
4153
/// </summary>

actions/sequester/Quest2GitHub/Options/EnvironmentVariableReader.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ internal sealed class EnvironmentVariableReader
55
internal static ApiKeys GetApiKeys()
66
{
77
var githubToken = CoalesceEnvVar(("ImportOptions__ApiKeys__GitHubToken", "GitHubKey"));
8-
var questKey = CoalesceEnvVar(("ImportOptions__ApiKeys__QuestKey", "QuestKey"));
8+
// This is optional so that developers can run the app locally without setting up the devOps token.
9+
var questToken = CoalesceEnvVar(("ImportOptions__ApiKeys__QuestAccessToken", "QuestAccessToken"), false);
910

1011
// These keys are used when the app is run as an org enabled action. They are optional.
1112
// If missing, the action runs using repo-only rights.
@@ -14,11 +15,15 @@ internal static ApiKeys GetApiKeys()
1415

1516
var azureAccessToken = CoalesceEnvVar(("ImportOptions__ApiKeys__AzureAccessToken", "AZURE_ACCESS_TOKEN"), false);
1617

18+
// This key is the PAT for Quest access. It's now a legacy key. Secretless should be better.
19+
var questKey = CoalesceEnvVar(("ImportOptions__ApiKeys__QuestKey", "QuestKey"), false);
20+
1721
if (!int.TryParse(appIDString, out int appID)) appID = 0;
1822

1923
return new ApiKeys()
2024
{
2125
GitHubToken = githubToken,
26+
QuestAccessToken = questToken,
2227
AzureAccessToken = azureAccessToken,
2328
QuestKey = questKey,
2429
SequesterPrivateKey = oauthPrivateKey,

actions/sequester/Quest2GitHub/QuestGitHubService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class QuestGitHubService(
3131
IGitHubClient ghClient,
3232
OspoClient? ospoClient,
3333
string azdoKey,
34+
bool useBearerToken,
3435
string questOrg,
3536
string questProject,
3637
string areaPath,
@@ -41,7 +42,7 @@ public class QuestGitHubService(
4142
IEnumerable<LabelToTagMap> tagMap) : IDisposable
4243
{
4344
private const string LinkedWorkItemComment = "Associated WorkItem - ";
44-
private readonly QuestClient _azdoClient = new(azdoKey, questOrg, questProject);
45+
private readonly QuestClient _azdoClient = new(azdoKey, questOrg, questProject, useBearerToken);
4546
private readonly OspoClient? _ospoClient = ospoClient;
4647
private readonly string _questLinkString = $"https://dev.azure.com/{questOrg}/{questProject}/_workitems/edit/";
4748

0 commit comments

Comments
 (0)