Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder
builder.Services.AddSingleton<IDiagnosticEntry, EndpointUsageDiagnosticEntry>();
builder.Services.AddSingleton<ClientLoadedTracker>();
builder.Services.AddSingleton<IDiagnosticEntry, ClientInfoDiagnosticEntry>();
builder.Services.AddSingleton<ResourceLoadedTracker>();
builder.Services.AddSingleton<IDiagnosticEntry, ResourceInfoDiagnosticEntry>();
builder.Services.AddSingleton<DiagnosticSummary>();
builder.Services.AddHostedService<DiagnosticHostedService>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Text.Json;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;

internal class ResourceInfoDiagnosticEntry(ResourceLoadedTracker resourceLoadedTracker) : IDiagnosticEntry
{
public Task WriteAsync(Utf8JsonWriter writer)
{
writer.WriteStartObject("Resources");

var resourceGroups =
resourceLoadedTracker.Resources.GroupBy(resource => resource.Value.Type, resource => resource.Value);
foreach (var group in resourceGroups)
{
writer.WriteStartArray(group.Key);

if (group.Key == "ApiResource")
{
WriteApiResources(writer, group);
}
else
{
WriteSimpleResources(writer, group);
}

writer.WriteEndArray();
}

writer.WriteEndObject();

return Task.CompletedTask;
}

private static void WriteApiResources(Utf8JsonWriter writer, IEnumerable<TrackedResource> apiResources)
{
foreach (var resource in apiResources)
{
writer.WriteStartObject();
writer.WriteString("Name", resource.Name);
writer.WriteBoolean("ResourceIndicatorRequired", resource.ResourceIndicatorRequired.GetValueOrDefault());
writer.WriteStartArray("SecretTypes");
foreach (var secretType in resource.SecretTypes ?? [])
{
writer.WriteStringValue(secretType);
}

writer.WriteEndArray();

writer.WriteEndObject();
}
}

private static void WriteSimpleResources(Utf8JsonWriter writer, IEnumerable<TrackedResource> resources)
{
foreach (var resource in resources)
{
writer.WriteStringValue(resource.Name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

#nullable enable
using System.Collections.Concurrent;
using Duende.IdentityServer.Models;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics;

internal class ResourceLoadedTracker
{
private const int MaxResourceTrackedCount = 100;

private int _resourceCount;
private readonly ConcurrentDictionary<string, TrackedResource> _resources = new();

public void TrackResources(Resources resources)
{
foreach (var resourcesApiResource in resources.ApiResources)
{
TrackResource(resourcesApiResource);
}

foreach (var resourcesIdentityResource in resources.IdentityResources)
{
TrackResource(resourcesIdentityResource);
}

foreach (var resourcesApiScope in resources.ApiScopes)
{
TrackResource(resourcesApiScope);
}
}

public IReadOnlyDictionary<string, TrackedResource> Resources => _resources;

private void TrackResource(ApiResource apiResource)
{
if (_resourceCount >= MaxResourceTrackedCount)
{
return;
}

if (_resources.ContainsKey($"ApiResource:{apiResource.Name}"))
{
return;
}

var resource = new TrackedResource("ApiResource",
apiResource.Name,
apiResource.RequireResourceIndicator,
apiResource.ApiSecrets?.Select(secret => secret.Type).Distinct());

if (_resources.TryAdd($"ApiResource:{apiResource.Name}", resource))
{
Interlocked.Increment(ref _resourceCount);
}
}

private void TrackResource(IdentityResource identityResource)
{
if (_resourceCount >= MaxResourceTrackedCount)
{
return;
}

if (_resources.ContainsKey($"IdentityResource:{identityResource.Name}"))
{
return;
}

var resource = new TrackedResource("IdentityResource",
identityResource.Name,
null,
null);

if (_resources.TryAdd($"IdentityResource:{identityResource.Name}", resource))
{
Interlocked.Increment(ref _resourceCount);
}
}

private void TrackResource(ApiScope apiScope)
{
if (_resourceCount >= MaxResourceTrackedCount)
{
return;
}

if (_resources.ContainsKey($"ApiScope:{apiScope.Name}"))
{
return;
}

var resource = new TrackedResource("ApiScope",
apiScope.Name,
null,
null);

if (_resources.TryAdd($"ApiScope:{apiScope.Name}", resource))
{
Interlocked.Increment(ref _resourceCount);
}
}
}

internal record TrackedResource(
string Type,
string Name,
bool? ResourceIndicatorRequired,
IEnumerable<string>? SecretTypes);
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal class AuthorizeRequestValidator : IAuthorizeRequestValidator
private readonly IRequestObjectValidator _requestObjectValidator;
private readonly LicenseUsageTracker _licenseUsage;
private readonly ClientLoadedTracker _clientLoadedTracker;
private readonly ResourceLoadedTracker _resourceLoadedTracker;
private readonly SanitizedLogger<AuthorizeRequestValidator> _sanitizedLogger;

private readonly ResponseTypeEqualityComparer
Expand All @@ -47,6 +48,7 @@ public AuthorizeRequestValidator(
IRequestObjectValidator requestObjectValidator,
LicenseUsageTracker licenseUsage,
ClientLoadedTracker clientLoadedTracker,
ResourceLoadedTracker resourceLoadedTracker,
SanitizedLogger<AuthorizeRequestValidator> sanitizedLogger)
{
_options = options;
Expand All @@ -59,6 +61,7 @@ public AuthorizeRequestValidator(
_userSession = userSession;
_licenseUsage = licenseUsage;
_clientLoadedTracker = clientLoadedTracker;
_resourceLoadedTracker = resourceLoadedTracker;
_sanitizedLogger = sanitizedLogger;
}

Expand Down Expand Up @@ -150,6 +153,7 @@ public async Task<AuthorizeRequestValidationResult> ValidateAsync(
_licenseUsage.ClientUsed(request.ClientId);
_clientLoadedTracker.TrackClientLoaded(request.Client);
IdentityServerLicenseValidator.Instance.ValidateClient(request.ClientId);
_resourceLoadedTracker.TrackResources(request.ValidatedResources.Resources);

return Valid(request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal class TokenRequestValidator : ITokenRequestValidator
private readonly IClock _clock;
private readonly LicenseUsageTracker _licenseUsage;
private readonly ClientLoadedTracker _clientLoadedTracker;
private readonly ResourceLoadedTracker _resourceLoadedTracker;
private readonly ILogger _logger;

private ValidatedTokenRequest _validatedRequest;
Expand All @@ -62,6 +63,7 @@ public TokenRequestValidator(
IClock clock,
LicenseUsageTracker licenseUsage,
ClientLoadedTracker clientLoadedTracker,
ResourceLoadedTracker resourceLoadedTracker,
ILogger<TokenRequestValidator> logger)
{
_logger = logger;
Expand All @@ -83,6 +85,7 @@ public TokenRequestValidator(
_dPoPProofValidator = dPoPProofValidator;
_events = events;
_clientLoadedTracker = clientLoadedTracker;
_resourceLoadedTracker = resourceLoadedTracker;
}

// only here for legacy unit tests
Expand Down Expand Up @@ -311,6 +314,7 @@ private async Task<TokenRequestValidationResult> RunValidationAsync(Func<NameVal
_licenseUsage.ClientUsed(clientId);
_clientLoadedTracker.TrackClientLoaded(customValidationContext.Result.ValidatedRequest.Client);
IdentityServerLicenseValidator.Instance.ValidateClient(clientId);
_resourceLoadedTracker.TrackResources(customValidationContext.Result.ValidatedRequest.ValidatedResources.Resources);

return customValidationContext.Result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using Duende.IdentityModel.Client;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
using Duende.IdentityServer.Models;
using IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries;

namespace IdentityServer.UnitTests.Licensing.v2.DiagnosticEntries;

public class ResourceInfoDiagnosticEntryTests
{
[Fact]
public async Task Should_Write_Resource_Info()
{
var tracker = new ResourceLoadedTracker();
tracker.TrackResources(new Resources
{
ApiResources = new List<ApiResource>
{
new("TestApi", "Test API")
{
RequireResourceIndicator = true,
ApiSecrets = new List<Secret> { new Secret { Type = "Test" } }
}
},
IdentityResources = new List<IdentityResource>
{
new("TestIdentity", ["IrrelevantClaim"])
},
ApiScopes = new List<ApiScope>
{
new("TestScope", "Test Scope")
}
});
var subject = new ResourceInfoDiagnosticEntry(tracker);

var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);

var resources = result.RootElement.GetProperty("Resources");
var apiResources = resources.GetProperty("ApiResource").EnumerateArray();
var apiResource = apiResources.First();
apiResource.GetProperty("Name").GetString().ShouldBe("TestApi");
apiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeTrue();
apiResource.TryGetStringArray("SecretTypes").ShouldBe(["Test"]);
resources.TryGetStringArray("IdentityResource").ShouldBe(["TestIdentity"]);
resources.TryGetStringArray("ApiScope").ShouldBe(["TestScope"]);
}

[Fact]
public async Task Should_Write_Empty_Resource_Info()
{
var tracker = new ResourceLoadedTracker();
var subject = new ResourceInfoDiagnosticEntry(tracker);

var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);

result.RootElement.GetProperty("Resources").GetRawText().ShouldBe("{}");
}

[Fact]
public async Task Should_Write_Resource_Info_With_Empty_Secret_Types()
{
var tracker = new ResourceLoadedTracker();
tracker.TrackResources(new Resources
{
ApiResources = new List<ApiResource>
{
new("TestApi", "Test API")
{
RequireResourceIndicator = true,
ApiSecrets = new List<Secret>()
}
}
});
var subject = new ResourceInfoDiagnosticEntry(tracker);

var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);

var resources = result.RootElement.GetProperty("Resources");
var apiResources = resources.GetProperty("ApiResource").EnumerateArray();
var apiResource = apiResources.First();
apiResource.GetProperty("Name").GetString().ShouldBe("TestApi");
apiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeTrue();
apiResource.TryGetStringArray("SecretTypes").ShouldBe([]);
}

[Fact]
public async Task Should_Write_Multiple_Resources()
{
var tracker = new ResourceLoadedTracker();
tracker.TrackResources(new Resources
{
ApiResources = new List<ApiResource>
{
new("ApiResourceOne", "Test API 1")
{
RequireResourceIndicator = true
},
new("ApiResourceTwo", "Test API 2")
{
RequireResourceIndicator = false,
ApiSecrets = new List<Secret> { new() { Type = "SecretType" } }
}
},
IdentityResources = new List<IdentityResource>
{
new("IdentityResourceOne", ["Claim1"]),
new("IdentityResourceTwo", ["Claim2"])
},
ApiScopes = new List<ApiScope>
{
new("ApiScopeOne", "Test Scope 1"),
new("ApiScopeTwo", "Test Scope 2")
}
});
var subject = new ResourceInfoDiagnosticEntry(tracker);

var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);

var resources = result.RootElement.GetProperty("Resources");
var apiResources = resources.GetProperty("ApiResource").EnumerateArray().OrderBy(resource => resource.GetProperty("Name").GetString()).ToList();
var firstApiResource = apiResources.First();
firstApiResource.GetProperty("Name").GetString().ShouldBe("ApiResourceOne");
firstApiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeTrue();
firstApiResource.TryGetStringArray("SecretTypes").ShouldBe([]);
var secondApiResource = apiResources.Last();
secondApiResource.GetProperty("Name").GetString().ShouldBe("ApiResourceTwo");
secondApiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeFalse();
secondApiResource.TryGetStringArray("SecretTypes").ShouldBe(["SecretType"]);
resources.TryGetStringArray("IdentityResource").ShouldBe(["IdentityResourceOne", "IdentityResourceTwo"], ignoreOrder: true);
resources.TryGetStringArray("ApiScope").ShouldBe(["ApiScopeOne", "ApiScopeTwo"], ignoreOrder: true);
}
}
Loading
Loading