diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs index f3f8fe3e8..c247cd674 100644 --- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs +++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs @@ -214,6 +214,8 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(new ServiceCollectionAccessor(builder.Services)); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/RegisteredImplementationsDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/RegisteredImplementationsDiagnosticEntry.cs new file mode 100644 index 000000000..911b4b04d --- /dev/null +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/RegisteredImplementationsDiagnosticEntry.cs @@ -0,0 +1,210 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using System.Text.Json; +using Duende.IdentityServer.Hosting; +using Duende.IdentityServer.Internal; +using Duende.IdentityServer.ResponseHandling; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Services.KeyManagement; +using Duende.IdentityServer.Stores; +using Duende.IdentityServer.Stores.Serialization; +using Duende.IdentityServer.Validation; + +namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; + +internal class RegisteredImplementationsDiagnosticEntry(ServiceCollectionAccessor serviceCollectionAccessor) + : IDiagnosticEntry +{ + private readonly Dictionary> _typesToInspect = new() + { + { + "Root", [ typeof(IIdentityServerTools) ] + }, + { + "Hosting", [ + typeof(IEndpointHandler), + typeof(IEndpointResult), + typeof(IEndpointRouter), + typeof(IHttpResponseWriter<>) + ] + }, + { + "Infrastructure", [typeof(IClock), typeof(IConcurrencyLock<>)] + }, + { + "ResponseHandling", [ + typeof(IAuthorizeInteractionResponseGenerator), + typeof(IAuthorizeResponseGenerator), + typeof(IBackchannelAuthenticationResponseGenerator), + typeof(IDeviceAuthorizationResponseGenerator), + typeof(IDiscoveryResponseGenerator), + typeof(IIntrospectionResponseGenerator), + typeof(IPushedAuthorizationResponseGenerator), + typeof(ITokenResponseGenerator), + typeof(ITokenRevocationResponseGenerator), + typeof(IUserInfoResponseGenerator) + ] + }, + { + "Services", [ + typeof(IAutomaticKeyManagerKeyStore), + typeof(IBackchannelAuthenticationInteractionService), + typeof(IBackchannelAuthenticationThrottlingService), + typeof(IBackchannelAuthenticationUserNotificationService), + typeof(IBackChannelLogoutHttpClient), + typeof(IBackChannelLogoutService), + typeof(ICache<>), + typeof(ICancellationTokenProvider), + typeof(IClaimsService), + typeof(IConsentService), + typeof(ICorsPolicyService), + typeof(IDeviceFlowCodeService), + typeof(IDeviceFlowInteractionService), + typeof(IDeviceFlowThrottlingService), + typeof(IEventService), + typeof(IEventSink), + typeof(IHandleGenerationService), + typeof(IIdentityServerInteractionService), + typeof(IIssuerNameService), + typeof(IJwtRequestUriHttpClient), + typeof(IKeyManager), + typeof(IKeyMaterialService), + typeof(ILogoutNotificationService), + typeof(IPersistedGrantService), + typeof(IProfileService), + typeof(IPushedAuthorizationSerializer), + typeof(IPushedAuthorizationService), + typeof(IRefreshTokenService), + typeof(IReplayCache), + typeof(IReturnUrlParser), + typeof(IServerUrls), + typeof(ISessionCoordinationService), + typeof(ISessionManagementService), + typeof(ISigningKeyProtector), + typeof(ISigningKeyStoreCache), + typeof(ITokenCreationService), + typeof(ITokenService), + typeof(IUserCodeGenerator), + typeof(IUserCodeService), + typeof(IUserSession) + ] + }, + { + "Stores", [ + typeof(IAuthorizationCodeStore), + typeof(IAuthorizationParametersMessageStore), + typeof(IBackChannelAuthenticationRequestStore), + typeof(IClientStore), + typeof(IConsentMessageStore), + typeof(IDeviceFlowStore), + typeof(IIdentityProviderStore), + typeof(IMessageStore<>), + typeof(IPersistentGrantSerializer), + typeof(IPersistedGrantStore), + typeof(IPushedAuthorizationRequestStore), + typeof(IReferenceTokenStore), + typeof(IRefreshTokenStore), + typeof(IResourceStore), + typeof(IServerSideSessionsMarker), + typeof(IServerSideSessionStore), + typeof(IServerSideTicketStore), + typeof(ISigningCredentialStore), + typeof(ISigningKeyStore), + typeof(IUserConsentStore), + typeof(IValidationKeysStore) + ] + }, + { + "Validation", [ + typeof(IApiSecretValidator), + typeof(IAuthorizeRequestValidator), + typeof(IBackchannelAuthenticationRequestIdValidator), + typeof(IBackchannelAuthenticationRequestValidator), + typeof(IBackchannelAuthenticationUserValidator), + typeof(IClientConfigurationValidator), + typeof(IClientSecretValidator), + typeof(ICustomAuthorizeRequestValidator), + typeof(ICustomBackchannelAuthenticationValidator), + typeof(ICustomTokenRequestValidator), + typeof(ICustomTokenValidator), + typeof(IDeviceAuthorizationRequestValidator), + typeof(IDeviceCodeValidator), + typeof(IDPoPProofValidator), + typeof(IEndSessionRequestValidator), + typeof(IExtensionGrantValidator), + typeof(IIdentityProviderConfigurationValidator), + typeof(IIntrospectionRequestValidator), + typeof(IJwtRequestValidator), + typeof(IPushedAuthorizationRequestValidator), + typeof(IRedirectUriValidator), + typeof(IResourceOwnerPasswordValidator), + typeof(IResourceValidator), + typeof(IScopeParser), + typeof(ISecretParser), + typeof(ISecretsListParser), + typeof(ISecretsListValidator), + typeof(ISecretValidator), + typeof(ITokenRequestValidator), + typeof(ITokenRevocationRequestValidator), + typeof(ITokenValidator), + typeof(IUserInfoRequestValidator) + ] + } + }; + + public Task WriteAsync(Utf8JsonWriter writer) + { + writer.WriteStartObject("RegisteredImplementations"); + + foreach (var group in _typesToInspect) + { + writer.WriteStartArray(group.Key); + + foreach (var type in group.Value) + { + WriteImplementationDetails(type, type.Name, writer); + } + + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + + return Task.CompletedTask; + } + + private void WriteImplementationDetails(Type targetType, string serviceName, Utf8JsonWriter writer) + { + writer.WriteStartObject(); + writer.WriteStartArray(serviceName); + + var services = serviceCollectionAccessor.ServiceCollection.Where(descriptor => + descriptor.ServiceType == targetType && + descriptor.ImplementationType != null); + if (services.Any()) + { + foreach (var service in services) + { + var type = service.ImplementationType!; + writer.WriteStartObject(); + writer.WriteString("TypeName", type.FullName); + writer.WriteString("Assembly", type.Assembly.GetName().Name); + writer.WriteString("AssemblyVersion", type.Assembly.GetName().Version?.ToString()); + writer.WriteEndObject(); + } + } + else + { + writer.WriteStartObject(); + writer.WriteString("TypeName", "Not Registered"); + writer.WriteString("Assembly", "Not Registered"); + writer.WriteString("AssemblyVersion", "Not Registered"); + writer.WriteEndObject(); + } + + + writer.WriteEndArray(); + writer.WriteEndObject(); + } +} diff --git a/identity-server/src/IdentityServer/Licensing/V2/ServiceCollectionAccessor.cs b/identity-server/src/IdentityServer/Licensing/V2/ServiceCollectionAccessor.cs new file mode 100644 index 000000000..7bf8a156a --- /dev/null +++ b/identity-server/src/IdentityServer/Licensing/V2/ServiceCollectionAccessor.cs @@ -0,0 +1,11 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; + +namespace Duende.IdentityServer.Licensing.V2; + +internal class ServiceCollectionAccessor(IServiceCollection serviceCollection) +{ + public IServiceCollection ServiceCollection { get; } = serviceCollection; +} diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/RegisteredImplementationsDiagnosticEntryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/RegisteredImplementationsDiagnosticEntryTests.cs new file mode 100644 index 000000000..3150513a9 --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/RegisteredImplementationsDiagnosticEntryTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Duende.IdentityServer.Licensing.V2; +using Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; +using Duende.IdentityServer.Services; +using Microsoft.Extensions.DependencyInjection; +using UnitTests.Common; + +namespace IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries; + +public class RegisteredImplementationsDiagnosticEntryTests +{ + [Fact] + public async Task WriteAsync_ShouldWriteRegisteredImplementationInfo() + { + var serviceCollection = new ServiceCollection() + .AddSingleton(); + var subject = new RegisteredImplementationsDiagnosticEntry(new ServiceCollectionAccessor(serviceCollection)); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject); + + var registeredImplementations = result.RootElement.GetProperty("RegisteredImplementations"); + var services = registeredImplementations.GetProperty("Services"); + var profileServiceEntry = services.EnumerateArray().ToList().SingleOrDefault(entry => entry.TryGetProperty(nameof(IProfileService), out _)); + var assemblyInfo = profileServiceEntry.GetProperty(nameof(IProfileService)).EnumerateArray().First(); + var expectedTypeInfo = typeof(MockProfileService); + assemblyInfo.GetProperty("TypeName").GetString().ShouldBe(expectedTypeInfo.FullName); + assemblyInfo.GetProperty("Assembly").GetString().ShouldBe(expectedTypeInfo.Assembly.GetName().Name); + assemblyInfo.GetProperty("AssemblyVersion").GetString().ShouldBe(expectedTypeInfo.Assembly.GetName().Version?.ToString()); + } + + [Fact] + public async Task WriteAsync_GroupsImplementationsByCategory() + { + var subject = new RegisteredImplementationsDiagnosticEntry(new ServiceCollectionAccessor(new ServiceCollection())); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject); + + var registeredImplementations = result.RootElement.GetProperty("RegisteredImplementations"); + registeredImplementations.TryGetProperty("Root", out _).ShouldBeTrue(); + registeredImplementations.TryGetProperty("Hosting", out _).ShouldBeTrue(); + registeredImplementations.TryGetProperty("Infrastructure", out _).ShouldBeTrue(); + registeredImplementations.TryGetProperty("ResponseHandling", out _).ShouldBeTrue(); + registeredImplementations.TryGetProperty("Services", out _).ShouldBeTrue(); + registeredImplementations.TryGetProperty("Stores", out _).ShouldBeTrue(); + registeredImplementations.TryGetProperty("Validation", out _).ShouldBeTrue(); + } + + [Fact] + public async Task WriteAsync_HandlesMultipleRegistrationsForAService() + { + var serviceCollection = new ServiceCollection() + .AddSingleton() + .AddSingleton(); + var subject = new RegisteredImplementationsDiagnosticEntry(new ServiceCollectionAccessor(serviceCollection)); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject); + + var registeredImplementations = result.RootElement.GetProperty("RegisteredImplementations"); + var services = registeredImplementations.GetProperty("Services"); + var profileServiceEntry = services.EnumerateArray().ToList().SingleOrDefault(entry => entry.TryGetProperty(nameof(IProfileService), out _)); + var firstAssemblyInfo = profileServiceEntry.GetProperty(nameof(IProfileService)).EnumerateArray().First(); + var firstExpectedTypeInfo = typeof(DefaultProfileService); + firstAssemblyInfo.GetProperty("TypeName").GetString().ShouldBe(firstExpectedTypeInfo.FullName); + firstAssemblyInfo.GetProperty("Assembly").GetString().ShouldBe(firstExpectedTypeInfo.Assembly.GetName().Name); + firstAssemblyInfo.GetProperty("AssemblyVersion").GetString().ShouldBe(firstExpectedTypeInfo.Assembly.GetName().Version?.ToString()); + var secondAssemblyInfo = profileServiceEntry.GetProperty(nameof(IProfileService)).EnumerateArray().Last(); + var secondExpectedTypeInfo = typeof(MockProfileService); + secondAssemblyInfo.GetProperty("TypeName").GetString().ShouldBe(secondExpectedTypeInfo.FullName); + secondAssemblyInfo.GetProperty("Assembly").GetString().ShouldBe(secondExpectedTypeInfo.Assembly.GetName().Name); + secondAssemblyInfo.GetProperty("AssemblyVersion").GetString().ShouldBe(secondExpectedTypeInfo.Assembly.GetName().Version?.ToString()); + } + + [Fact] + public async Task WriteAsync_HandlesNoServiceRegisteredForInterface() + { + var subject = new RegisteredImplementationsDiagnosticEntry(new ServiceCollectionAccessor(new ServiceCollection())); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject); + + var registeredImplementations = result.RootElement.GetProperty("RegisteredImplementations"); + var services = registeredImplementations.GetProperty("Services"); + var profileServiceEntry = services.EnumerateArray().ToList().SingleOrDefault(entry => entry.TryGetProperty(nameof(IProfileService), out _)); + var assemblyInfo = profileServiceEntry.GetProperty(nameof(IProfileService)).EnumerateArray().First(); + assemblyInfo.GetProperty("TypeName").GetString().ShouldBe("Not Registered"); + assemblyInfo.GetProperty("Assembly").GetString().ShouldBe("Not Registered"); + assemblyInfo.GetProperty("AssemblyVersion").GetString().ShouldBe("Not Registered"); + } + + [Fact] + public async Task WriteAsync_ShouldIncludeAllPublicInterfaces() + { + var interfaces = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetExportedTypes()) + .Where(type => type.IsInterface && type.IsPublic && type.Namespace != null && + type.Namespace.StartsWith( + "Duende.IdentityServer")) + .Select(type => type.Name); + var subject = new RegisteredImplementationsDiagnosticEntry(new ServiceCollectionAccessor(new ServiceCollection())); + + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject); + + var registeredImplementations = result.RootElement.GetProperty("RegisteredImplementations"); + var entries = registeredImplementations.EnumerateObject() + .SelectMany(property => property.Value.EnumerateArray()).Select(element => element.EnumerateObject().First().Name); + entries.ShouldBe(interfaces, ignoreOrder: true); + } +}