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
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-workitems-rw:
type: azure-devops-access-token
parameters:
organizations: dnceng
scopes: work_write
domainAccountName: dn-bot
domainAccountSecret:
name: dn-bot-account-redmond
location: helixkv

dn-bot-dnceng-build-r-code-r-project-r-profile-r:
type: azure-devops-access-token
parameters:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
{
"HealthReportSettings": {
"StorageAccountTablesUri": "https://helixexecution.table.core.windows.net",
"ManagedIdentityClientId": "d2580e46-e758-4778-a864-18f909438b45"
},
"KeyVaultUri": "https://DotNetEng-Status-Prod.vault.azure.net/",
"GitHub": {
"Organization": "dotnet",
"Repository": "dnceng",
"NotificationTargets": [ "dotnet/dnceng", "dotnet/prodconsvcs" ],
"AlertLabels": [ "Ops - First Responder", "Critical" ],
"EnvironmentLabels": [ "Production" ],
"TitlePrefix": "Production - "
},
"DataProtection": {
"KeyBlobUri": "https://dotnetengstatusprod.blob.core.windows.net/site/keys.xml",
"DataProtectionKeyUri": "https://dotneteng-status-prod.vault.azure.net/keys/dotnet-status-data-protection/"
},
"AzureTableTokenStore": {
"TableUri": "https://dotnetengstatusprod.table.core.windows.net"
},
"Grafana": {
"BaseUrl": "https://dotnet-eng-grafana.westus2.cloudapp.azure.com",
"TableUri": "https://dotnetengstatusprod.table.core.windows.net"
},
"Kusto": {
"Database": "engineeringdata",
"KustoClusterUri": "https://engsrvprod.westus.kusto.windows.net",
"KustoIngestionUri": "https://ingest-engsrvprod.westus.kusto.windows.net",
"ManagedIdentityId": "d2580e46-e758-4778-a864-18f909438b45",
"UseAzCliAuthentication": false
}
}
{
"HealthReportSettings": {
"StorageAccountTablesUri": "https://helixexecution.table.core.windows.net",
"ManagedIdentityClientId": "d2580e46-e758-4778-a864-18f909438b45"
},
"KeyVaultUri": "https://DotNetEng-Status-Prod.vault.azure.net/",
"GitHub": {
"Organization": "dotnet",
"Repository": "dnceng",
"NotificationTargets": [
"dotnet/dnceng",
"dotnet/prodconsvcs"
],
"AlertLabels": [
"Ops - First Responder",
"Critical"
],
"EnvironmentLabels": [
"Production"
],
"TitlePrefix": "Production - "
},
"DataProtection": {
"KeyBlobUri": "https://dotnetengstatusprod.blob.core.windows.net/site/keys.xml",
"DataProtectionKeyUri": "https://dotneteng-status-prod.vault.azure.net/keys/dotnet-status-data-protection/"
},
"AzureTableTokenStore": {
"TableUri": "https://dotnetengstatusprod.table.core.windows.net"
},
"Grafana": {
"BaseUrl": "https://dotnet-eng-grafana.westus2.cloudapp.azure.com",
"TableUri": "https://dotnetengstatusprod.table.core.windows.net"
},
"Kusto": {
"Database": "engineeringdata",
"KustoClusterUri": "https://engsrvprod.westus.kusto.windows.net",
"KustoIngestionUri": "https://ingest-engsrvprod.westus.kusto.windows.net",
"ManagedIdentityId": "d2580e46-e758-4778-a864-18f909438b45",
"UseAzCliAuthentication": false
},
"AzureDevOps": {
"dnceng": {
"UseManagedIdentity": true
}
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
{
"HealthReportSettings": {
"ManagedIdentityClientId": "e9d81917-4c98-44cc-8a6e-601311ac3c07"
},
"KeyVaultUri": "https://DotNetEng-Status-Staging.vault.azure.net/",
"GitHub": {
"Organization": "dotnet",
"Repository": "dnceng",
"NotificationTargets": [ "dotnet/dnceng", "dotnet/prodconsvcs" ],
"AlertLabels": [ "Ops - First Responder" ],
"EnvironmentLabels": [ "Staging" ],
"TitlePrefix": "Staging - "
},
"DataProtection": {
"KeyBlobUri": "https://dotnetengstatusstaging.blob.core.windows.net/site/keys.xml",
"DataProtectionKeyUri": "https://dotneteng-status-staging.vault.azure.net/keys/dotnet-status-data-protection/"
},
"MilestoneManagement": {
"ReposEnabledFor": [ "maestro-auth-test/webhook-test" ]
},
"AzureTableTokenStore": {
"TableUri": "https://dotnetengstatusstaging.table.core.windows.net"
},
"Grafana": {
"TableUri": "https://dotnetengstatusstaging.table.core.windows.net"
},
"Kusto": {
"Database": "engineeringdata",
"KustoClusterUri": "https://engdata.westus2.kusto.windows.net",
"KustoIngestionUri": "https://ingest-engdata.westus2.kusto.windows.net",
"ManagedIdentityId": "e9d81917-4c98-44cc-8a6e-601311ac3c07",
"UseAzCliAuthentication": false
}
}
{
"HealthReportSettings": {
"ManagedIdentityClientId": "e9d81917-4c98-44cc-8a6e-601311ac3c07"
},
"KeyVaultUri": "https://DotNetEng-Status-Staging.vault.azure.net/",
"GitHub": {
"Organization": "dotnet",
"Repository": "dnceng",
"NotificationTargets": [
"dotnet/dnceng",
"dotnet/prodconsvcs"
],
"AlertLabels": [
"Ops - First Responder"
],
"EnvironmentLabels": [
"Staging"
],
"TitlePrefix": "Staging - "
},
"DataProtection": {
"KeyBlobUri": "https://dotnetengstatusstaging.blob.core.windows.net/site/keys.xml",
"DataProtectionKeyUri": "https://dotneteng-status-staging.vault.azure.net/keys/dotnet-status-data-protection/"
},
"MilestoneManagement": {
"ReposEnabledFor": [
"maestro-auth-test/webhook-test"
]
},
"AzureTableTokenStore": {
"TableUri": "https://dotnetengstatusstaging.table.core.windows.net"
},
"Grafana": {
"TableUri": "https://dotnetengstatusstaging.table.core.windows.net"
},
"Kusto": {
"Database": "engineeringdata",
"KustoClusterUri": "https://engdata.westus2.kusto.windows.net",
"KustoIngestionUri": "https://ingest-engdata.westus2.kusto.windows.net",
"ManagedIdentityId": "e9d81917-4c98-44cc-8a6e-601311ac3c07",
"UseAzCliAuthentication": false
},
"AzureDevOps": {
"dnceng": {
"UseManagedIdentity": true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
},
"dnceng": {
"Organization": "dnceng",
"AccessToken": "[vault(dn-bot-dnceng-workitems-rw)]",
"MaxParallelRequests": 10
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public async Task ManagedIdentity_CanListBuilds_FromDncengInternal()
var options = new AzureDevOpsClientOptions
{
Organization = "dnceng",
ManagedIdentityClientId = "placeholder-activates-bearer-path",
UseManagedIdentity = true,
MaxParallelRequests = 1,
};

Expand Down Expand Up @@ -99,7 +99,7 @@ public async Task ManagedIdentity_CanGetTimeline_FromDncengInternal()
var options = new AzureDevOpsClientOptions
{
Organization = "dnceng",
ManagedIdentityClientId = "placeholder-activates-bearer-path",
UseManagedIdentity = true,
MaxParallelRequests = 1,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ public async Task Client_WithAccessToken_UsesBasicAuth()
}

/// <summary>
/// When ManagedIdentityClientId is configured (without AccessToken), requests
/// When UseManagedIdentity is true with a client ID (user-assigned MI), requests
/// should use Bearer authentication with a token obtained from the TokenCredential.
/// </summary>
[Test]
public async Task Client_WithManagedIdentity_UsesBearerAuth()
public async Task Client_WithUserAssignedManagedIdentity_UsesBearerAuth()
{
// Arrange
const string fakeToken = "fake-entra-bearer-token";
Expand All @@ -85,6 +85,7 @@ public async Task Client_WithManagedIdentity_UsesBearerAuth()
var options = new AzureDevOpsClientOptions
{
Organization = "test-org",
UseManagedIdentity = true,
ManagedIdentityClientId = "00000000-0000-0000-0000-000000000001",
MaxParallelRequests = 1,
};
Expand All @@ -102,7 +103,47 @@ public async Task Client_WithManagedIdentity_UsesBearerAuth()
}

/// <summary>
/// When ManagedIdentityClientId is configured, the client should request a token
/// When UseManagedIdentity is true without a client ID (system-assigned MI), requests
/// should use Bearer authentication with a token obtained from the TokenCredential.
/// </summary>
[Test]
public async Task Client_WithSystemAssignedManagedIdentity_UsesBearerAuth()
{
// Arrange
const string fakeToken = "fake-system-assigned-token";

var handler = new CapturingHandler(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
JsonConvert.SerializeObject(new { count = 0, value = Array.Empty<object>() }),
Encoding.UTF8,
"application/json")
});
var factory = new DelegatingHandlerHttpClientFactory(handler);

var mockCredential = new FakeTokenCredential(fakeToken);

var options = new AzureDevOpsClientOptions
{
Organization = "test-org",
UseManagedIdentity = true,
MaxParallelRequests = 1,
};

var client = new AzureDevOpsClient(options, _logger, factory, tokenCredential: mockCredential);

// Act
await client.ListBuilds("test-project", CancellationToken.None);

// Assert
Assert.That(handler.LastRequest, Is.Not.Null, "Expected at least one request to be captured");
Assert.That(handler.LastRequest!.Headers.Authorization, Is.Not.Null);
Assert.That(handler.LastRequest.Headers.Authorization!.Scheme, Is.EqualTo("Bearer"));
Assert.That(handler.LastRequest.Headers.Authorization.Parameter, Is.EqualTo(fakeToken));
}

/// <summary>
/// When UseManagedIdentity is configured, the client should request a token
/// for the Azure DevOps resource scope.
/// </summary>
[Test]
Expand All @@ -123,7 +164,7 @@ public async Task Client_WithManagedIdentity_RequestsCorrectScope()
var options = new AzureDevOpsClientOptions
{
Organization = "test-org",
ManagedIdentityClientId = "00000000-0000-0000-0000-000000000001",
UseManagedIdentity = true,
MaxParallelRequests = 1,
};

Expand All @@ -138,7 +179,7 @@ public async Task Client_WithManagedIdentity_RequestsCorrectScope()
}

/// <summary>
/// When AccessToken takes precedence over ManagedIdentityClientId if both are set.
/// AccessToken takes precedence over UseManagedIdentity if both are set.
/// </summary>
[Test]
public async Task Client_WithBothPatAndManagedIdentity_PrefersPatAuth()
Expand All @@ -162,6 +203,7 @@ public async Task Client_WithBothPatAndManagedIdentity_PrefersPatAuth()
{
Organization = "test-org",
AccessToken = pat,
UseManagedIdentity = true,
ManagedIdentityClientId = "00000000-0000-0000-0000-000000000001",
MaxParallelRequests = 1,
};
Expand All @@ -179,7 +221,7 @@ public async Task Client_WithBothPatAndManagedIdentity_PrefersPatAuth()
}

/// <summary>
/// When neither AccessToken nor ManagedIdentityClientId is set, no auth header
/// When neither AccessToken nor UseManagedIdentity is set, no auth header
/// should be present on requests.
/// </summary>
[Test]
Expand Down Expand Up @@ -231,7 +273,7 @@ public async Task Client_WithManagedIdentity_RefreshesTokenPerRequest()
var options = new AzureDevOpsClientOptions
{
Organization = "test-org",
ManagedIdentityClientId = "00000000-0000-0000-0000-000000000001",
UseManagedIdentity = true,
MaxParallelRequests = 1,
};

Expand Down
25 changes: 18 additions & 7 deletions src/Telemetry/AzureDevOpsClient/AzureDevOpsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,25 @@ public AzureDevOpsClient(
Convert.ToBase64String(Encoding.UTF8.GetBytes($":{options.AccessToken}"))
);
}
else if (!string.IsNullOrEmpty(options.ManagedIdentityClientId))
else if (options.UseManagedIdentity)
{
_logger.LogInformation(
"Using Managed Identity authentication (ClientId: {clientId}) for org {organization}",
options.ManagedIdentityClientId,
options.Organization);
_tokenCredential = tokenCredential
?? new ManagedIdentityCredential(options.ManagedIdentityClientId);
if (!string.IsNullOrEmpty(options.ManagedIdentityClientId))
{
_logger.LogInformation(
"Using user-assigned Managed Identity (ClientId: {clientId}) for org {organization}",
options.ManagedIdentityClientId,
options.Organization);
_tokenCredential = tokenCredential
?? new ManagedIdentityCredential(options.ManagedIdentityClientId);
}
else
{
_logger.LogInformation(
"Using system-assigned Managed Identity for org {organization}",
options.Organization);
_tokenCredential = tokenCredential
?? new ManagedIdentityCredential();
}
}
else
{
Expand Down
14 changes: 11 additions & 3 deletions src/Telemetry/AzureDevOpsClient/AzureDevOpsClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ public class AzureDevOpsClientOptions
public string AccessToken { get; set; }

/// <summary>
/// The client ID of the Managed Identity to use for Entra-based authentication.
/// When set (and <see cref="AccessToken"/> is not provided), the client will use
/// a Managed Identity to obtain a bearer token for Azure DevOps.
/// When <c>true</c>, the client authenticates to Azure DevOps using a Managed Identity
/// instead of a PAT. For system-assigned identities, leave <see cref="ManagedIdentityClientId"/>
/// empty. For user-assigned identities, also set <see cref="ManagedIdentityClientId"/>.
/// Ignored when <see cref="AccessToken"/> is provided.
/// </summary>
public bool UseManagedIdentity { get; set; }

/// <summary>
/// The client ID of a user-assigned Managed Identity. Only required when
/// <see cref="UseManagedIdentity"/> is <c>true</c> and the identity is user-assigned.
/// For system-assigned identities this should be left empty.
/// </summary>
public string ManagedIdentityClientId { get; set; }
}