Skip to content

Commit b876e8c

Browse files
committed
Update application to support secretless auth
Update the SeQuester C# application to support secretless authentication.
1 parent c5685a9 commit b876e8c

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
@@ -107,11 +107,21 @@ private static async Task<QuestGitHubService> CreateService(ImportOptions option
107107
{
108108
Console.WriteLine("Warning: Imported work items won't be assigned based on GitHub assignee.");
109109
}
110+
bool useBearerToken = (options.ApiKeys.QuestAccessToken is not null);
111+
string? token = useBearerToken ?
112+
options.ApiKeys.QuestAccessToken :
113+
options.ApiKeys.QuestKey;
114+
115+
if (string.IsNullOrWhiteSpace(token))
116+
{
117+
throw new InvalidOperationException("Azure DevOps token is missing.");
118+
}
110119

111120
return new QuestGitHubService(
112121
gitHubClient,
113122
ospoClient,
114-
options.ApiKeys.QuestKey,
123+
token,
124+
useBearerToken,
115125
options.AzureDevOps.Org,
116126
options.AzureDevOps.Project,
117127
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,
@@ -40,7 +41,7 @@ public class QuestGitHubService(
4041
IEnumerable<LabelToTagMap> tagMap) : IDisposable
4142
{
4243
private const string LinkedWorkItemComment = "Associated WorkItem - ";
43-
private readonly QuestClient _azdoClient = new(azdoKey, questOrg, questProject);
44+
private readonly QuestClient _azdoClient = new(azdoKey, questOrg, questProject, useBearerToken);
4445
private readonly OspoClient? _ospoClient = ospoClient;
4546
private readonly string _questLinkString = $"https://dev.azure.com/{questOrg}/{questProject}/_workitems/edit/";
4647

0 commit comments

Comments
 (0)