Skip to content

Commit c969789

Browse files
authored
Merge pull request #2047 from DuendeSoftware/beh/resource-info-diagnostic-entry
Resource Info Diagnostic Entry
2 parents 25121b3 + c4854d6 commit c969789

File tree

9 files changed

+403
-0
lines changed

9 files changed

+403
-0
lines changed

identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder
225225
builder.Services.AddSingleton<IDiagnosticEntry, EndpointUsageDiagnosticEntry>();
226226
builder.Services.AddSingleton<ClientLoadedTracker>();
227227
builder.Services.AddSingleton<IDiagnosticEntry, ClientInfoDiagnosticEntry>();
228+
builder.Services.AddSingleton<ResourceLoadedTracker>();
229+
builder.Services.AddSingleton<IDiagnosticEntry, ResourceInfoDiagnosticEntry>();
228230
builder.Services.AddSingleton<DiagnosticSummary>();
229231
builder.Services.AddHostedService<DiagnosticHostedService>();
230232

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Text.Json;
5+
6+
namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
7+
8+
internal class ResourceInfoDiagnosticEntry(ResourceLoadedTracker resourceLoadedTracker) : IDiagnosticEntry
9+
{
10+
public Task WriteAsync(Utf8JsonWriter writer)
11+
{
12+
writer.WriteStartObject("Resources");
13+
14+
var resourceGroups =
15+
resourceLoadedTracker.Resources.GroupBy(resource => resource.Value.Type, resource => resource.Value);
16+
foreach (var group in resourceGroups)
17+
{
18+
writer.WriteStartArray(group.Key);
19+
20+
if (group.Key == "ApiResource")
21+
{
22+
WriteApiResources(writer, group);
23+
}
24+
else
25+
{
26+
WriteSimpleResources(writer, group);
27+
}
28+
29+
writer.WriteEndArray();
30+
}
31+
32+
writer.WriteEndObject();
33+
34+
return Task.CompletedTask;
35+
}
36+
37+
private static void WriteApiResources(Utf8JsonWriter writer, IEnumerable<TrackedResource> apiResources)
38+
{
39+
foreach (var resource in apiResources)
40+
{
41+
writer.WriteStartObject();
42+
writer.WriteString("Name", resource.Name);
43+
writer.WriteBoolean("ResourceIndicatorRequired", resource.ResourceIndicatorRequired.GetValueOrDefault());
44+
writer.WriteStartArray("SecretTypes");
45+
foreach (var secretType in resource.SecretTypes ?? [])
46+
{
47+
writer.WriteStringValue(secretType);
48+
}
49+
50+
writer.WriteEndArray();
51+
52+
writer.WriteEndObject();
53+
}
54+
}
55+
56+
private static void WriteSimpleResources(Utf8JsonWriter writer, IEnumerable<TrackedResource> resources)
57+
{
58+
foreach (var resource in resources)
59+
{
60+
writer.WriteStringValue(resource.Name);
61+
}
62+
}
63+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
#nullable enable
5+
using System.Collections.Concurrent;
6+
using Duende.IdentityServer.Models;
7+
8+
namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
9+
10+
internal class ResourceLoadedTracker
11+
{
12+
private const int MaxResourceTrackedCount = 100;
13+
14+
private int _resourceCount;
15+
private readonly ConcurrentDictionary<string, TrackedResource> _resources = new();
16+
17+
public void TrackResources(Resources resources)
18+
{
19+
foreach (var resourcesApiResource in resources.ApiResources)
20+
{
21+
TrackResource(resourcesApiResource);
22+
}
23+
24+
foreach (var resourcesIdentityResource in resources.IdentityResources)
25+
{
26+
TrackResource(resourcesIdentityResource);
27+
}
28+
29+
foreach (var resourcesApiScope in resources.ApiScopes)
30+
{
31+
TrackResource(resourcesApiScope);
32+
}
33+
}
34+
35+
public IReadOnlyDictionary<string, TrackedResource> Resources => _resources;
36+
37+
private void TrackResource(ApiResource apiResource)
38+
{
39+
if (_resourceCount >= MaxResourceTrackedCount)
40+
{
41+
return;
42+
}
43+
44+
if (_resources.ContainsKey($"ApiResource:{apiResource.Name}"))
45+
{
46+
return;
47+
}
48+
49+
var resource = new TrackedResource("ApiResource",
50+
apiResource.Name,
51+
apiResource.RequireResourceIndicator,
52+
apiResource.ApiSecrets?.Select(secret => secret.Type).Distinct());
53+
54+
if (_resources.TryAdd($"ApiResource:{apiResource.Name}", resource))
55+
{
56+
Interlocked.Increment(ref _resourceCount);
57+
}
58+
}
59+
60+
private void TrackResource(IdentityResource identityResource)
61+
{
62+
if (_resourceCount >= MaxResourceTrackedCount)
63+
{
64+
return;
65+
}
66+
67+
if (_resources.ContainsKey($"IdentityResource:{identityResource.Name}"))
68+
{
69+
return;
70+
}
71+
72+
var resource = new TrackedResource("IdentityResource",
73+
identityResource.Name,
74+
null,
75+
null);
76+
77+
if (_resources.TryAdd($"IdentityResource:{identityResource.Name}", resource))
78+
{
79+
Interlocked.Increment(ref _resourceCount);
80+
}
81+
}
82+
83+
private void TrackResource(ApiScope apiScope)
84+
{
85+
if (_resourceCount >= MaxResourceTrackedCount)
86+
{
87+
return;
88+
}
89+
90+
if (_resources.ContainsKey($"ApiScope:{apiScope.Name}"))
91+
{
92+
return;
93+
}
94+
95+
var resource = new TrackedResource("ApiScope",
96+
apiScope.Name,
97+
null,
98+
null);
99+
100+
if (_resources.TryAdd($"ApiScope:{apiScope.Name}", resource))
101+
{
102+
Interlocked.Increment(ref _resourceCount);
103+
}
104+
}
105+
}
106+
107+
internal record TrackedResource(
108+
string Type,
109+
string Name,
110+
bool? ResourceIndicatorRequired,
111+
IEnumerable<string>? SecretTypes);

identity-server/src/IdentityServer/Validation/Default/AuthorizeRequestValidator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ internal class AuthorizeRequestValidator : IAuthorizeRequestValidator
3030
private readonly IRequestObjectValidator _requestObjectValidator;
3131
private readonly LicenseUsageTracker _licenseUsage;
3232
private readonly ClientLoadedTracker _clientLoadedTracker;
33+
private readonly ResourceLoadedTracker _resourceLoadedTracker;
3334
private readonly SanitizedLogger<AuthorizeRequestValidator> _sanitizedLogger;
3435

3536
private readonly ResponseTypeEqualityComparer
@@ -47,6 +48,7 @@ public AuthorizeRequestValidator(
4748
IRequestObjectValidator requestObjectValidator,
4849
LicenseUsageTracker licenseUsage,
4950
ClientLoadedTracker clientLoadedTracker,
51+
ResourceLoadedTracker resourceLoadedTracker,
5052
SanitizedLogger<AuthorizeRequestValidator> sanitizedLogger)
5153
{
5254
_options = options;
@@ -59,6 +61,7 @@ public AuthorizeRequestValidator(
5961
_userSession = userSession;
6062
_licenseUsage = licenseUsage;
6163
_clientLoadedTracker = clientLoadedTracker;
64+
_resourceLoadedTracker = resourceLoadedTracker;
6265
_sanitizedLogger = sanitizedLogger;
6366
}
6467

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

154158
return Valid(request);
155159
}

identity-server/src/IdentityServer/Validation/Default/TokenRequestValidator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ internal class TokenRequestValidator : ITokenRequestValidator
3939
private readonly IClock _clock;
4040
private readonly LicenseUsageTracker _licenseUsage;
4141
private readonly ClientLoadedTracker _clientLoadedTracker;
42+
private readonly ResourceLoadedTracker _resourceLoadedTracker;
4243
private readonly ILogger _logger;
4344

4445
private ValidatedTokenRequest _validatedRequest;
@@ -62,6 +63,7 @@ public TokenRequestValidator(
6263
IClock clock,
6364
LicenseUsageTracker licenseUsage,
6465
ClientLoadedTracker clientLoadedTracker,
66+
ResourceLoadedTracker resourceLoadedTracker,
6567
ILogger<TokenRequestValidator> logger)
6668
{
6769
_logger = logger;
@@ -83,6 +85,7 @@ public TokenRequestValidator(
8385
_dPoPProofValidator = dPoPProofValidator;
8486
_events = events;
8587
_clientLoadedTracker = clientLoadedTracker;
88+
_resourceLoadedTracker = resourceLoadedTracker;
8689
}
8790

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

315319
return customValidationContext.Result;
316320
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using Duende.IdentityModel.Client;
5+
using Duende.IdentityServer.Licensing.V2.Diagnostics;
6+
using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
7+
using Duende.IdentityServer.Models;
8+
using IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries;
9+
10+
namespace IdentityServer.UnitTests.Licensing.v2.DiagnosticEntries;
11+
12+
public class ResourceInfoDiagnosticEntryTests
13+
{
14+
[Fact]
15+
public async Task Should_Write_Resource_Info()
16+
{
17+
var tracker = new ResourceLoadedTracker();
18+
tracker.TrackResources(new Resources
19+
{
20+
ApiResources = new List<ApiResource>
21+
{
22+
new("TestApi", "Test API")
23+
{
24+
RequireResourceIndicator = true,
25+
ApiSecrets = new List<Secret> { new Secret { Type = "Test" } }
26+
}
27+
},
28+
IdentityResources = new List<IdentityResource>
29+
{
30+
new("TestIdentity", ["IrrelevantClaim"])
31+
},
32+
ApiScopes = new List<ApiScope>
33+
{
34+
new("TestScope", "Test Scope")
35+
}
36+
});
37+
var subject = new ResourceInfoDiagnosticEntry(tracker);
38+
39+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);
40+
41+
var resources = result.RootElement.GetProperty("Resources");
42+
var apiResources = resources.GetProperty("ApiResource").EnumerateArray();
43+
var apiResource = apiResources.First();
44+
apiResource.GetProperty("Name").GetString().ShouldBe("TestApi");
45+
apiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeTrue();
46+
apiResource.TryGetStringArray("SecretTypes").ShouldBe(["Test"]);
47+
resources.TryGetStringArray("IdentityResource").ShouldBe(["TestIdentity"]);
48+
resources.TryGetStringArray("ApiScope").ShouldBe(["TestScope"]);
49+
}
50+
51+
[Fact]
52+
public async Task Should_Write_Empty_Resource_Info()
53+
{
54+
var tracker = new ResourceLoadedTracker();
55+
var subject = new ResourceInfoDiagnosticEntry(tracker);
56+
57+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);
58+
59+
result.RootElement.GetProperty("Resources").GetRawText().ShouldBe("{}");
60+
}
61+
62+
[Fact]
63+
public async Task Should_Write_Resource_Info_With_Empty_Secret_Types()
64+
{
65+
var tracker = new ResourceLoadedTracker();
66+
tracker.TrackResources(new Resources
67+
{
68+
ApiResources = new List<ApiResource>
69+
{
70+
new("TestApi", "Test API")
71+
{
72+
RequireResourceIndicator = true,
73+
ApiSecrets = new List<Secret>()
74+
}
75+
}
76+
});
77+
var subject = new ResourceInfoDiagnosticEntry(tracker);
78+
79+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);
80+
81+
var resources = result.RootElement.GetProperty("Resources");
82+
var apiResources = resources.GetProperty("ApiResource").EnumerateArray();
83+
var apiResource = apiResources.First();
84+
apiResource.GetProperty("Name").GetString().ShouldBe("TestApi");
85+
apiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeTrue();
86+
apiResource.TryGetStringArray("SecretTypes").ShouldBe([]);
87+
}
88+
89+
[Fact]
90+
public async Task Should_Write_Multiple_Resources()
91+
{
92+
var tracker = new ResourceLoadedTracker();
93+
tracker.TrackResources(new Resources
94+
{
95+
ApiResources = new List<ApiResource>
96+
{
97+
new("ApiResourceOne", "Test API 1")
98+
{
99+
RequireResourceIndicator = true
100+
},
101+
new("ApiResourceTwo", "Test API 2")
102+
{
103+
RequireResourceIndicator = false,
104+
ApiSecrets = new List<Secret> { new() { Type = "SecretType" } }
105+
}
106+
},
107+
IdentityResources = new List<IdentityResource>
108+
{
109+
new("IdentityResourceOne", ["Claim1"]),
110+
new("IdentityResourceTwo", ["Claim2"])
111+
},
112+
ApiScopes = new List<ApiScope>
113+
{
114+
new("ApiScopeOne", "Test Scope 1"),
115+
new("ApiScopeTwo", "Test Scope 2")
116+
}
117+
});
118+
var subject = new ResourceInfoDiagnosticEntry(tracker);
119+
120+
var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject);
121+
122+
var resources = result.RootElement.GetProperty("Resources");
123+
var apiResources = resources.GetProperty("ApiResource").EnumerateArray().OrderBy(resource => resource.GetProperty("Name").GetString()).ToList();
124+
var firstApiResource = apiResources.First();
125+
firstApiResource.GetProperty("Name").GetString().ShouldBe("ApiResourceOne");
126+
firstApiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeTrue();
127+
firstApiResource.TryGetStringArray("SecretTypes").ShouldBe([]);
128+
var secondApiResource = apiResources.Last();
129+
secondApiResource.GetProperty("Name").GetString().ShouldBe("ApiResourceTwo");
130+
secondApiResource.GetProperty("ResourceIndicatorRequired").GetBoolean().ShouldBeFalse();
131+
secondApiResource.TryGetStringArray("SecretTypes").ShouldBe(["SecretType"]);
132+
resources.TryGetStringArray("IdentityResource").ShouldBe(["IdentityResourceOne", "IdentityResourceTwo"], ignoreOrder: true);
133+
resources.TryGetStringArray("ApiScope").ShouldBe(["ApiScopeOne", "ApiScopeTwo"], ignoreOrder: true);
134+
}
135+
}

0 commit comments

Comments
 (0)