Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions .vault-config/service-connections-devdiv.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ secrets:
name: dn-bot-account-redmond
organizations: dnceng
scopes: packaging_write

dnceng-test-tools-feed:
type: azure-devops-service-endpoint
parameters:
Expand Down Expand Up @@ -127,17 +127,4 @@ secrets:
location: helixkv
name: dn-bot-account-redmond
organizations: dnceng
scopes: packaging_write

dnceng/internal:
type: azure-devops-service-endpoint
parameters:
authorization:
type: azure-devops-access-token
parameters:
domainAccountName: dn-bot
domainAccountSecret:
location: helixkv
name: dn-bot-account-redmond
organizations: dnceng
scopes: code
scopes: packaging_write
10 changes: 0 additions & 10 deletions .vault-config/shared/dotneteng-status-secrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ app-insights-connection-string:
parameters:
description: The connection string for application insights. Go to the Azure resource for application insights -> Configure -> Properties -> Get the connection string

dn-bot-dnceng-build-rw-code-rw-release-rw:
type: azure-devops-access-token
parameters:
organizations: dnceng
scopes: build_execute code_write release_execute
domainAccountName: dn-bot
domainAccountSecret:
name: dn-bot-account-redmond
location: helixkv

dn-bot-dnceng-workitems-rw:
type: azure-devops-access-token
parameters:
Expand Down
10 changes: 0 additions & 10 deletions .vault-config/shared/telemetry-secrets.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
dn-bot-dnceng-build-r:
type: azure-devops-access-token
parameters:
domainAccountName: dn-bot
domainAccountSecret:
location: helixkv
name: dn-bot-account-redmond
organizations: dnceng
scopes: build

dn-bot-dnceng-public-build-r:
type: azure-devops-access-token
parameters:
Expand Down
4 changes: 4 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ extends:
displayName: 'Publish Secret Manager Scenario Tests'
targetPath: $(Build.SourcesDirectory)\artifacts\bin\Microsoft.DncEng.SecretManager.ScenarioTests\$(_BuildConfig)\net8.0
artifactName: Microsoft.DncEng.SecretManager.ScenarioTests
- output: pipelineArtifact
displayName: 'Publish AzureDevOpsClient Post-Deployment Tests'
targetPath: $(Build.SourcesDirectory)\artifacts\bin\AzureDevOpsClient.PostDeploymentTests\$(_BuildConfig)\net8.0
artifactName: AzureDevOpsClient.PostDeploymentTests
variables:
# DotNet-Blob-Feed provides: dotnetfeed-storage-access-key-1
# DotNet-Symbol-Server-Pats provides: microsoft-symbol-server-pat, symweb-symbol-server-pat
Expand Down
11 changes: 11 additions & 0 deletions dnceng.sln
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Monitoring
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Monitoring.Sdk.Tests", "src\Monitoring\Microsoft.DotNet.Monitoring.Sdk.Tests\Microsoft.DotNet.Monitoring.Sdk.Tests.csproj", "{02046914-EAB2-4128-BD3E-06C8730B9D9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDevOpsClient.PostDeploymentTests", "src\Telemetry\AzureDevOpsClient.PostDeploymentTests\AzureDevOpsClient.PostDeploymentTests.csproj", "{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -203,6 +205,14 @@ Global
{02046914-EAB2-4128-BD3E-06C8730B9D9F}.Release|Any CPU.Build.0 = Release|Any CPU
{02046914-EAB2-4128-BD3E-06C8730B9D9F}.Release|x64.ActiveCfg = Release|Any CPU
{02046914-EAB2-4128-BD3E-06C8730B9D9F}.Release|x64.Build.0 = Release|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Debug|x64.ActiveCfg = Debug|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Debug|x64.Build.0 = Debug|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Release|Any CPU.Build.0 = Release|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Release|x64.ActiveCfg = Release|Any CPU
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -223,6 +233,7 @@ Global
{2DBCBD85-13B6-41DD-A00F-C40116C46B61} = {EC7A0A22-5BCD-4B94-8E17-510A54E42ED1}
{BC62FD8B-FC12-434F-9958-4EA0A52D60B2} = {4614FF66-8594-4D7E-BEF8-31611B6087C5}
{02046914-EAB2-4128-BD3E-06C8730B9D9F} = {4614FF66-8594-4D7E-BEF8-31611B6087C5}
{CEBE08DA-1A16-4ED2-B552-654FB51ACBF8} = {095B825A-91B7-441A-BF18-3A59838F477A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C72EF6D0-2510-41EB-87F3-7739C6C6D53B}
Expand Down
23 changes: 23 additions & 0 deletions eng/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,26 @@ stages:
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
displayName: Secret Manager Scenario Tests

- job: telemetryValidation
displayName: Telemetry MI auth validation
timeoutInMinutes: 15
steps:
- download: current
artifact: AzureDevOpsClient.PostDeploymentTests

- task: UseDotNet@2
displayName: Install .NET 8
inputs:
version: 8.x

- task: AzureCLI@2
inputs:
azureSubscription: 'secret-manager-scenario-tests'
scriptType: 'ps'
scriptLocation: 'inlineScript'
inlineScript: |
dotnet test $(Pipeline.Workspace)/AzureDevOpsClient.PostDeploymentTests/AzureDevOpsClient.PostDeploymentTests.dll --filter "TestCategory=PostDeployment" --logger "trx;LogFilePrefix=TelemetryMIAuth"
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
displayName: Telemetry Managed Identity Auth Tests
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,13 @@ public override async Task<List<SecretData>> RotateValues(Parameters parameters,

if (parameters.HasWebhookSecret)
{
webhookSecret = await _console.PromptAndValidateAsync("Webhook Secret",
"is required",
l => !string.IsNullOrWhiteSpace(l));
bool changeWebhookSecret = isNew || await _console.ConfirmAsync("Do you want to change the webhook secret? (yes/no): ");
if (changeWebhookSecret)
{
webhookSecret = await _console.PromptAndValidateAsync("Webhook Secret",
"is required",
l => !string.IsNullOrWhiteSpace(l));
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
<SignAssembly>false</SignAssembly>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AzureDevOpsClient\AzureDevOpsClient.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.DotNet.Internal.AzureDevOps.PostDeploymentTests;

/// <summary>
/// Post-deployment tests that validate bearer token (Entra ID) authentication
/// to Azure DevOps works correctly for the telemetry service.
///
/// In CI these run in the validateDeployment pipeline stage using an
/// <see cref="AzurePipelinesCredential"/> backed by the pipeline service
/// connection. Locally they fall back to <see cref="AzureCliCredential"/>
/// from an active <c>az login</c> session.
///
/// These tests exercise the same code path that the deployed service uses
/// with its Managed Identity — only the token source differs.
/// </summary>
[TestFixture]
[Category("PostDeployment")]
public class TelemetryManagedIdentityTests
{
private TokenCredential _credential = null!;
private ILogger<AzureDevOpsClient> _logger = null!;

[OneTimeSetUp]
public void Setup()
{
_logger = LoggerFactory
.Create(b => b.AddConsole())
.CreateLogger<AzureDevOpsClient>();

// Pipeline environment: use AzurePipelinesCredential from the service connection
// (same pattern as SecretManager ScenarioTestsBase).
string? clientId = Environment.GetEnvironmentVariable("AZURESUBSCRIPTION_CLIENT_ID");
string? tenantId = Environment.GetEnvironmentVariable("AZURESUBSCRIPTION_TENANT_ID");
string? serviceConnectionId = Environment.GetEnvironmentVariable("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID");
string? systemAccessToken = Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN");

if (!string.IsNullOrEmpty(clientId)
&& !string.IsNullOrEmpty(tenantId)
&& !string.IsNullOrEmpty(serviceConnectionId)
&& !string.IsNullOrEmpty(systemAccessToken))
{
_credential = new AzurePipelinesCredential(tenantId, clientId, serviceConnectionId, systemAccessToken);
}
else
{
// Local dev: fall back to az login
_credential = new AzureCliCredential();
}
}

/// <summary>
/// Validates that the Managed Identity can acquire a bearer token for
/// Azure DevOps and successfully list builds from dnceng/internal.
/// </summary>
[Test]
public async Task ManagedIdentity_CanListBuilds_FromDncengInternal()
{
var options = new AzureDevOpsClientOptions
{
Organization = "dnceng",
ManagedIdentityClientId = "placeholder-activates-bearer-path",
MaxParallelRequests = 1,
};

var client = new AzureDevOpsClient(options, _logger, new SimpleHttpClientFactory(), _credential);

var builds = await client.ListBuilds("internal", CancellationToken.None, limit: 3);

Assert.That(builds, Is.Not.Null);
Assert.That(builds.Length, Is.GreaterThan(0),
"Expected at least one build from dnceng/internal using bearer token auth");

TestContext.Out.WriteLine($"Retrieved {builds.Length} build(s) via Managed Identity:");
foreach (var build in builds)
{
TestContext.Out.WriteLine($" Build #{build.Id} — {build.Definition?.Name} — {build.Status}");
}
}

/// <summary>
/// Validates that the Managed Identity can read build timeline data,
/// which is the core operation the telemetry service performs.
/// </summary>
[Test]
public async Task ManagedIdentity_CanGetTimeline_FromDncengInternal()
{
var options = new AzureDevOpsClientOptions
{
Organization = "dnceng",
ManagedIdentityClientId = "placeholder-activates-bearer-path",
MaxParallelRequests = 1,
};

var client = new AzureDevOpsClient(options, _logger, new SimpleHttpClientFactory(), _credential);

// First get a recent build
var builds = await client.ListBuilds("internal", CancellationToken.None, limit: 1);
Assert.That(builds, Is.Not.Null);
Assert.That(builds.Length, Is.GreaterThan(0), "Need at least one build to test timeline access");

var build = builds[0];
TestContext.Out.WriteLine($"Fetching timeline for build #{build.Id}...");

var timeline = await client.GetTimelineAsync("internal", (int)build.Id, CancellationToken.None);

Assert.That(timeline, Is.Not.Null, "Expected a non-null timeline from dnceng/internal build");
TestContext.Out.WriteLine($"Timeline has {timeline!.Records?.Length ?? 0} record(s)");
}

/// <summary>
/// Validates that the bearer token uses the correct Azure DevOps scope
/// by requesting a token directly and inspecting it succeeds.
/// </summary>
[Test]
public async Task ManagedIdentity_CanAcquireAzDoToken()
{
var context = new TokenRequestContext(
new[] { AzureDevOpsClient.AzureDevOpsResourceId });

var token = await _credential.GetTokenAsync(context, CancellationToken.None);

Assert.That(token.Token, Is.Not.Null.And.Not.Empty,
"Expected a non-empty bearer token for Azure DevOps");
Assert.That(token.ExpiresOn, Is.GreaterThan(DateTimeOffset.UtcNow),
"Token should not already be expired");

TestContext.Out.WriteLine($"Token acquired (expires {token.ExpiresOn:u}, length={token.Token.Length})");
}

private sealed class SimpleHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name) => new HttpClient();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Core" />
<PackageReference Include="Microsoft.DotNet.Internal.Testing.Utility" />
</ItemGroup>

Expand Down
Loading