Skip to content

Commit

Permalink
Add service account login to abstract devops store base class (#8082)
Browse files Browse the repository at this point in the history
  • Loading branch information
hallipr authored Apr 15, 2024
1 parent f37db1a commit 1640470
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private static IDictionary<string, Func<StoreConfiguration, SecretStore>> GetDef
[KeyVaultCertificateStore.MappingKey] = KeyVaultCertificateStore.GetSecretStoreFactory(tokenCredential, logger),
[ManualActionStore.MappingKey] = ManualActionStore.GetSecretStoreFactory(logger, new ConsoleValueProvider()),
[ServiceAccountPersonalAccessTokenStore.MappingKey] = ServiceAccountPersonalAccessTokenStore.GetSecretStoreFactory(tokenCredential, new SecretProvider(logger), logger),
[ServiceConnectionParameterStore.MappingKey] = ServiceConnectionParameterStore.GetSecretStoreFactory(tokenCredential, logger),
[ServiceConnectionParameterStore.MappingKey] = ServiceConnectionParameterStore.GetSecretStoreFactory(tokenCredential, new SecretProvider(logger), logger),
[AadApplicationSecretStore.MappingKey] = AadApplicationSecretStore.GetSecretStoreFactory(tokenCredential, logger),
[AzureWebsiteStore.MappingKey] = AzureWebsiteStore.GetSecretStoreFactory(tokenCredential, logger),
[AadApplicationSecretStore.MappingKey] = AadApplicationSecretStore.GetSecretStoreFactory(tokenCredential, logger),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Azure.Core;
using Azure.Identity;
using Azure.Sdk.Tools.SecretRotation.Azure;
using Azure.Sdk.Tools.SecretRotation.Core;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using AccessToken = Azure.Core.AccessToken;

namespace Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps;

public abstract class AzureDevOpsStore : SecretStore
{
private const string AzureDevOpsApplicationId = "499b84ac-1321-427f-aa17-267ca6975798";
private const string AzureCliApplicationId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";

protected readonly ILogger logger;
protected readonly string organization;
private readonly string? serviceAccountTenantId;
private readonly string? serviceAccountName;
private readonly Uri? serviceAccountPasswordSecret;
private readonly TokenCredential tokenCredential;
private readonly ISecretProvider secretProvider;

protected AzureDevOpsStore(ILogger logger,
string organization,
string? serviceAccountTenantId,
string? serviceAccountName,
Uri? serviceAccountPasswordSecret,
TokenCredential tokenCredential,
ISecretProvider secretProvider)
{
this.logger = logger;
this.organization = organization;
this.serviceAccountTenantId = serviceAccountTenantId;
this.serviceAccountName = serviceAccountName;
this.serviceAccountPasswordSecret = serviceAccountPasswordSecret;
this.tokenCredential = tokenCredential;
this.secretProvider = secretProvider;
}

protected async Task<VssConnection> GetConnectionAsync()
{
string[] scopes = { "499b84ac-1321-427f-aa17-267ca6975798/.default" };
string? parentRequestId = null;

VssCredentials vssCredentials = this.serviceAccountPasswordSecret != null
? await GetServiceAccountCredentialsAsync()
: await GetTokenPrincipleCredentialsAsync(scopes, parentRequestId);

VssConnection connection = new(new Uri($"https://vssps.dev.azure.com/{this.organization}"), vssCredentials);

await connection.ConnectAsync();

return connection;
}

private static async Task<AccessToken> GetDevopsAadBearerTokenAsync(TokenCredential credential)
{
string[] scopes = { $"{AzureDevOpsApplicationId}/.default" };

var tokenRequestContext = new TokenRequestContext(scopes, parentRequestId: null);

AccessToken authenticationResult = await credential.GetTokenAsync(tokenRequestContext, CancellationToken.None);

return authenticationResult;
}

private async Task<VssCredentials> GetTokenPrincipleCredentialsAsync(string[] scopes, string? parentRequestId)
{
TokenRequestContext tokenRequestContext = new (scopes, parentRequestId);

AccessToken authenticationResult = await this.tokenCredential.GetTokenAsync(
tokenRequestContext,
CancellationToken.None);

VssAadCredential credentials = new (new VssAadToken("Bearer", authenticationResult.Token));

return credentials;
}

private async Task<VssCredentials> GetServiceAccountCredentialsAsync()
{
if (this.serviceAccountPasswordSecret == null)
{
throw new ArgumentNullException(nameof(serviceAccountPasswordSecret));
}

this.logger.LogDebug("Getting service account password from secret '{SecretName}'", this.serviceAccountPasswordSecret);

string serviceAccountPassword = await this.secretProvider.GetSecretValueAsync(this.tokenCredential, this.serviceAccountPasswordSecret);

this.logger.LogDebug("Getting token and devops client for 'dev.azure.com/{Organization}'", this.organization);

UsernamePasswordCredential serviceAccountCredential = new (this.serviceAccountName, serviceAccountPassword, this.serviceAccountTenantId, AzureCliApplicationId);

AccessToken token = await GetDevopsAadBearerTokenAsync(serviceAccountCredential);

VssAadCredential credentials = new (new VssAadToken("Bearer", token.Token));

return credentials;
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,27 @@
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.Core;
using Azure.Identity;
using Azure.Sdk.Tools.SecretRotation.Azure;
using Azure.Sdk.Tools.SecretRotation.Configuration;
using Azure.Sdk.Tools.SecretRotation.Core;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.DelegatedAuthorization;
using Microsoft.VisualStudio.Services.DelegatedAuthorization.Client;
using Microsoft.VisualStudio.Services.WebApi;
using AccessToken = Azure.Core.AccessToken;

namespace Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps;

public class ServiceAccountPersonalAccessTokenStore : SecretStore
public class ServiceAccountPersonalAccessTokenStore : AzureDevOpsStore
{
public const string MappingKey = "Service Account ADO PAT";
private const string AzureDevOpsApplicationId = "499b84ac-1321-427f-aa17-267ca6975798";
private const string AzureCliApplicationId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";

private readonly ILogger logger;
private readonly string organization;
private readonly string patDisplayName;
private readonly string scopes;
private readonly string serviceAccountTenantId;
private readonly string serviceAccountName;
private readonly Uri serviceAccountPasswordSecret;
private readonly RevocationAction revocationAction;
private readonly TokenCredential tokenCredential;
private readonly ISecretProvider secretProvider;

private ServiceAccountPersonalAccessTokenStore(ILogger logger,
private ServiceAccountPersonalAccessTokenStore(
ILogger logger,
string organization,
string patDisplayName,
string scopes,
Expand All @@ -42,17 +31,18 @@ private ServiceAccountPersonalAccessTokenStore(ILogger logger,
RevocationAction revocationAction,
TokenCredential tokenCredential,
ISecretProvider secretProvider)
: base(
logger,
organization,
serviceAccountTenantId,
serviceAccountName,
serviceAccountPasswordSecret,
tokenCredential,
secretProvider)
{
this.logger = logger;
this.organization = organization;
this.patDisplayName = patDisplayName;
this.scopes = scopes;
this.serviceAccountTenantId = serviceAccountTenantId;
this.serviceAccountName = serviceAccountName;
this.serviceAccountPasswordSecret = serviceAccountPasswordSecret;
this.revocationAction = revocationAction;
this.tokenCredential = tokenCredential;
this.secretProvider = secretProvider;
}

public override bool CanOriginate => true;
Expand Down Expand Up @@ -131,7 +121,8 @@ public override async Task<SecretValue> OriginateValueAsync(SecretState currentS

PatTokenResult result = await client.CreatePatAsync(new PatTokenCreateRequest { AllOrgs = false, DisplayName = this.patDisplayName, Scope = this.scopes, ValidTo = expirationDate.UtcDateTime });

if (result.PatTokenError != SessionTokenError.None) {
if (result.PatTokenError != SessionTokenError.None)
{
throw new RotationException($"Unable to create PAT: {result.PatTokenError}");
}

Expand All @@ -147,17 +138,6 @@ public override async Task<SecretValue> OriginateValueAsync(SecretState currentS
};
}

private static async Task<AccessToken> GetDevopsBearerTokenAsync(TokenCredential credential)
{
string[] scopes = { $"{AzureDevOpsApplicationId}/.default" };

var tokenRequestContext = new TokenRequestContext(scopes, parentRequestId: null);

AccessToken authenticationResult = await credential.GetTokenAsync(tokenRequestContext, CancellationToken.None);

return authenticationResult;
}

public override Func<Task>? GetRevocationActionAsync(SecretState secretState, bool whatIf)
{
if (!secretState.Tags.TryGetValue("AdoPatAuthorizationId", out string? authorizationIdString) ||
Expand Down Expand Up @@ -204,32 +184,6 @@ private static async Task<AccessToken> GetDevopsBearerTokenAsync(TokenCredential
};
}

private static async Task<VssCredentials> GetVssCredentials(TokenCredential credential)
{
AccessToken token = await GetDevopsBearerTokenAsync(credential);

return new VssAadCredential(new VssAadToken("Bearer", token.Token));
}

private async Task<VssConnection> GetConnectionAsync()
{
this.logger.LogDebug("Getting service account password from secret '{SecretName}'", this.serviceAccountPasswordSecret);

string serviceAccountPassword = await this.secretProvider.GetSecretValueAsync(this.tokenCredential, this.serviceAccountPasswordSecret);

this.logger.LogDebug("Getting token and devops client for 'dev.azure.com/{Organization}'", this.organization);

var serviceAccountCredential = new UsernamePasswordCredential(this.serviceAccountName, serviceAccountPassword, this.serviceAccountTenantId, AzureCliApplicationId);

VssCredentials vssCredentials = await GetVssCredentials(serviceAccountCredential);

var connection = new VssConnection(new Uri($"https://vssps.dev.azure.com/{this.organization}"), vssCredentials);

await connection.ConnectAsync();

return connection;
}

private enum RevocationAction
{
[JsonPropertyName("none")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.Core;
using Azure.Sdk.Tools.SecretRotation.Azure;
using Azure.Sdk.Tools.SecretRotation.Configuration;
using Azure.Sdk.Tools.SecretRotation.Core;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.ServiceEndpoints.WebApi;
using Microsoft.VisualStudio.Services.WebApi;

namespace Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps;

public class ServiceConnectionParameterStore : SecretStore
public class ServiceConnectionParameterStore : AzureDevOpsStore
{
public const string MappingKey = "ADO Service Connection Parameter";

private readonly string accountName;
private readonly string connectionId;
private readonly TokenCredential credential;
private readonly ILogger logger;
private readonly string parameterName;
private readonly string projectName;

public ServiceConnectionParameterStore(string accountName, string projectName, string connectionId,
string parameterName, TokenCredential credential, ILogger logger)
public ServiceConnectionParameterStore(
string organization,
string projectName,
string connectionId,
string parameterName,
string? serviceAccountTenantId,
string? serviceAccountName,
Uri? serviceAccountPasswordSecret,
TokenCredential tokenCredential,
ISecretProvider secretProvider,
ILogger logger)
: base(
logger,
organization,
serviceAccountTenantId,
serviceAccountName,
serviceAccountPasswordSecret,
tokenCredential,
secretProvider)
{
this.accountName = accountName;
this.projectName = projectName;
this.connectionId = connectionId;
this.parameterName = parameterName;
this.credential = credential;
this.logger = logger;
}

public override bool CanWrite => true;

public static Func<StoreConfiguration, SecretStore> GetSecretStoreFactory(TokenCredential credential,
ISecretProvider secretProvider,
ILogger logger)
{
return configuration =>
Expand All @@ -61,20 +73,39 @@ public static Func<StoreConfiguration, SecretStore> GetSecretStoreFactory(TokenC
throw new Exception("Missing required parameter 'parameterName'");
}

var passwordSecretParsed = Uri.TryCreate(parameters.ServiceAccountPasswordSecret, UriKind.Absolute, out Uri? serviceAccountPasswordSecret);

if (!string.IsNullOrEmpty(parameters.ServiceAccountName))
{
if (string.IsNullOrEmpty(parameters.ServiceAccountPasswordSecret))
{
throw new Exception("Missing required parameter 'serviceAccountPasswordSecret'");
}

if (!passwordSecretParsed)
{
throw new Exception("Unable to parse Uri from parameter 'serviceAccountPasswordSecret'");
}
}

return new ServiceConnectionParameterStore(
parameters.AccountName,
parameters.ProjectName,
parameters.ConnectionId,
parameters.ParameterName,
parameters.ServiceAccountTenantId,
parameters.ServiceAccountName,
serviceAccountPasswordSecret,
credential,
secretProvider,
logger);
};
}

public override async Task WriteSecretAsync(SecretValue secretValue, SecretState currentState,
DateTimeOffset? revokeAfterDate, bool whatIf)
{
this.logger.LogDebug("Getting token and devops client for dev.azure.com/{AccountName}", this.accountName);
this.logger.LogDebug("Getting token and devops client for dev.azure.com/{Organization}", this.organization);
VssConnection connection = await GetConnectionAsync();
var client = await connection.GetClientAsync<ServiceEndpointHttpClient>();

Expand Down Expand Up @@ -106,26 +137,6 @@ public override async Task WriteSecretAsync(SecretValue secretValue, SecretState
}
}

private async Task<VssConnection> GetConnectionAsync()
{
string[] scopes = { "499b84ac-1321-427f-aa17-267ca6975798/.default" };
string? parentRequestId = null;

var tokenRequestContext = new TokenRequestContext(scopes, parentRequestId);

AccessToken authenticationResult = await this.credential.GetTokenAsync(
tokenRequestContext,
CancellationToken.None);

var connection = new VssConnection(
new Uri($"https://dev.azure.com/{this.accountName}"),
new VssAadCredential(new VssAadToken("Bearer", authenticationResult.Token)));

await connection.ConnectAsync();

return connection;
}

private class Parameters
{
[JsonPropertyName("accountName")]
Expand All @@ -139,5 +150,15 @@ private class Parameters

[JsonPropertyName("parameterName")]
public string? ParameterName { get; set; }

[JsonPropertyName("serviceAccountTenantId")]
public string? ServiceAccountTenantId { get; set; }

[JsonPropertyName("serviceAccountName")]
public string? ServiceAccountName { get; set; }

[JsonPropertyName("serviceAccountPasswordSecret")]
public string? ServiceAccountPasswordSecret { get; set; }

}
}

0 comments on commit 1640470

Please sign in to comment.