From e76fbcab42fbb5db9b4a0ef4c02879408955d652 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:22:24 +0000 Subject: [PATCH 1/2] Add KeycloakRealmResource and AddRealm method This commit introduces a new `AddRealm` method in the `KeycloakResourceBuilderExtensions` class, enabling the addition of Keycloak Realm resources to the application model. The method constructs a `KeycloakRealmResource` using an `IResourceBuilder`, a realm name, and an optional realm name parameter. Additionally, a new `KeycloakRealmResource` class is defined, which includes properties for various Keycloak endpoints and their expressions, along with a constructor and detailed XML documentation. Changes to `PublicAPI.Unshipped.txt` reflect the inclusion of the new method and class in the public API. --- .../KeycloakRealmResource.cs | 105 ++++++++++++++++++ .../KeycloakResourceBuilderExtensions.cs | 22 ++++ 2 files changed, 127 insertions(+) create mode 100644 src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs diff --git a/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs new file mode 100644 index 0000000000..4e9cba662a --- /dev/null +++ b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Keycloak; + +/// +/// Represents a Keycloak Realm resource. +/// +/// The name of the realm resource. +/// The name of the realm. +/// The Keycloak server resource associated with this database. +public sealed class KeycloakRealmResource(string name, string realmName, KeycloakResource parent) : Resource(name), IResourceWithParent, IResourceWithConnectionString +{ + private EndpointReference? _parentEndpoint; + private EndpointReference ParentEndpoint => _parentEndpoint ??= new(Parent, "http"); + + /// + public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{ParentEndpoint.Property(EndpointProperty.Url)}/realms/{RealmName}/"); + + /// + /// Gets the issuer expression for the Keycloak realm. + /// + public ReferenceExpression IssuerExpression => ReferenceExpression.Create( + $"{ParentEndpoint.Property(EndpointProperty.Url)}/realms/{RealmName}"); + + /// + /// Gets or sets the metadata address for the Keycloak realm. + /// + public string MetadataAddress { get; set; } = ".well-known/openid-configuration"; + + /// + /// Gets the metadata address expression for the Keycloak realm. + /// + public ReferenceExpression MetadataAddressExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{MetadataAddress}"); + + /// + /// Gets or sets the 'authorization_endpoint' for the Keycloak realm. + /// + public string AuthorizationEndpoint { get; set; } = "protocol/openid-connect/auth"; + + /// + /// Gets the 'authorization_endpoint' expression for the Keycloak realm. + /// + public ReferenceExpression AuthorizationEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{AuthorizationEndpoint}"); + + /// + /// Gets or sets the 'token_endpoint' for the Keycloak realm. + /// + public string TokenEndpoint { get; set; } = "protocol/openid-connect/token"; + + /// + /// Gets the 'token_endpoint' expression for the Keycloak realm. + /// + public ReferenceExpression TokenEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{TokenEndpoint}"); + + /// + /// Gets or sets the 'introspection_endpoint' for the Keycloak realm. + /// + public string IntrospectionEndpoint { get; set; } = "protocol/openid-connect/token/introspect"; + + /// + /// Gets the 'introspection_endpoint' expression for the Keycloak realm. + /// + public ReferenceExpression IntrospectionEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{IntrospectionEndpoint}"); + + /// + /// Gets or sets 'user_info_endpoint' for the Keycloak realm. + /// + public string UserInfoEndpoint { get; set; } = "protocol/openid-connect/userinfo"; + + /// + /// Gets 'user_info_endpoint' expression for the Keycloak realm. + /// + public ReferenceExpression UserInfoEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{UserInfoEndpoint}"); + + /// + /// Gets or sets the 'end_session_endpoint' for the Keycloak realm. + /// + public string EndSessionEndpoint { get; set; } = "protocol/openid-connect/logout"; + + /// + /// Gets the 'end_session_endpoint' expression for the Keycloak realm. + /// + public ReferenceExpression EndSessionEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{EndSessionEndpoint}"); + + /// + /// Gets or sets the 'registration_endpoint' for the Keycloak realm. + /// + public string RegistrationEndpoint { get; set; } = "clients-registrations/openid-connect"; + + /// + /// Gets the 'registration_endpoint' expression for the Keycloak realm. + /// + public ReferenceExpression RegistrationEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{RegistrationEndpoint}"); + + /// + public KeycloakResource Parent { get; } = parent; + + /// + /// Gets the name of the realm. + /// + public string RealmName { get; } = realmName; +} diff --git a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs index ffd6758a38..dee6360ff6 100644 --- a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs @@ -175,4 +175,26 @@ public static IResourceBuilder WithRealmImport( throw new InvalidOperationException($"The realm import file or directory '{importFullPath}' does not exist."); } + + /// + /// Adds a Keycloak Realm to the application model from a . + /// + /// The Keycloak server resource builder. + /// The name of the realm. + /// The name of the realm. If not provided, the resource name will be used. + /// A reference to the . + public static IResourceBuilder AddRealm( + this IResourceBuilder builder, + string name, + string? realmName = null) + { + ArgumentNullException.ThrowIfNull(builder); + + // Use the resource name as the realm name if it's not provided + realmName ??= name; + + var keycloakRealm = new KeycloakRealmResource(name, realmName, builder.Resource); + + return builder.ApplicationBuilder.AddResource(keycloakRealm); + } } From e50368b0d7db7fa02a0b25e714f48b2967d872f8 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:47:23 +0000 Subject: [PATCH 2/2] Refactor KeycloakRealmResource and enhance validation The `KeycloakRealmResource` class has been refactored to include null checks in its constructor for parameters `name`, `realmName`, and `parent`. A private field `_parentEndpoint` and a property `ParentEndpoint` have been added. The `Parent` and `RealmName` properties now rely on the constructor for initialization. New unit tests in `KeycloakPublicApiTests.cs` ensure that the constructor throws an `ArgumentNullException` for null parameters. Additional tests validate that the `AddRealm` method correctly handles null values for the builder and realm name, improving input validation across the API. --- .../KeycloakRealmResource.cs | 58 +++++++++++----- .../KeycloakPublicApiTests.cs | 68 +++++++++++++++++++ 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs index 4e9cba662a..6070e77ad7 100644 --- a/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs +++ b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs @@ -8,27 +8,51 @@ namespace Aspire.Hosting.Keycloak; /// /// Represents a Keycloak Realm resource. /// -/// The name of the realm resource. -/// The name of the realm. -/// The Keycloak server resource associated with this database. -public sealed class KeycloakRealmResource(string name, string realmName, KeycloakResource parent) : Resource(name), IResourceWithParent, IResourceWithConnectionString +public sealed class KeycloakRealmResource : Resource, IResourceWithParent, IResourceWithConnectionString { private EndpointReference? _parentEndpoint; - private EndpointReference ParentEndpoint => _parentEndpoint ??= new(Parent, "http"); + private EndpointReferenceExpression? _parentUrl; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the realm resource. + /// The name of the realm. + /// The Keycloak server resource associated with this database. + public KeycloakRealmResource(string name, string realmName, KeycloakResource parent) : base(name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(realmName); + ArgumentNullException.ThrowIfNull(parent); + + RealmName = realmName; + RealmPath = $"realms/{realmName}"; + Parent = parent; + } + + private EndpointReferenceExpression ParentUrl => _parentUrl ??= ParentEndpoint.Property(EndpointProperty.Url); + + /// + /// Gets the parent endpoint reference. + /// + public EndpointReference ParentEndpoint => _parentEndpoint ??= new(Parent, "http"); /// - public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{ParentEndpoint.Property(EndpointProperty.Url)}/realms/{RealmName}/"); + public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{ParentUrl}/{RealmPath}/"); + + /// + /// Gets the base address of the realm. + /// + public string RealmPath { get; } /// /// Gets the issuer expression for the Keycloak realm. /// - public ReferenceExpression IssuerExpression => ReferenceExpression.Create( - $"{ParentEndpoint.Property(EndpointProperty.Url)}/realms/{RealmName}"); + public ReferenceExpression IssuerUrlExpression => ReferenceExpression.Create($"{ParentUrl}/{RealmPath}"); /// /// Gets or sets the metadata address for the Keycloak realm. /// - public string MetadataAddress { get; set; } = ".well-known/openid-configuration"; + public string MetadataAddress => ".well-known/openid-configuration"; /// /// Gets the metadata address expression for the Keycloak realm. @@ -38,7 +62,7 @@ public sealed class KeycloakRealmResource(string name, string realmName, Keycloa /// /// Gets or sets the 'authorization_endpoint' for the Keycloak realm. /// - public string AuthorizationEndpoint { get; set; } = "protocol/openid-connect/auth"; + public string AuthorizationEndpoint => "protocol/openid-connect/auth"; /// /// Gets the 'authorization_endpoint' expression for the Keycloak realm. @@ -48,7 +72,7 @@ public sealed class KeycloakRealmResource(string name, string realmName, Keycloa /// /// Gets or sets the 'token_endpoint' for the Keycloak realm. /// - public string TokenEndpoint { get; set; } = "protocol/openid-connect/token"; + public string TokenEndpoint => "protocol/openid-connect/token"; /// /// Gets the 'token_endpoint' expression for the Keycloak realm. @@ -58,7 +82,7 @@ public sealed class KeycloakRealmResource(string name, string realmName, Keycloa /// /// Gets or sets the 'introspection_endpoint' for the Keycloak realm. /// - public string IntrospectionEndpoint { get; set; } = "protocol/openid-connect/token/introspect"; + public string IntrospectionEndpoint => "protocol/openid-connect/token/introspect"; /// /// Gets the 'introspection_endpoint' expression for the Keycloak realm. @@ -68,7 +92,7 @@ public sealed class KeycloakRealmResource(string name, string realmName, Keycloa /// /// Gets or sets 'user_info_endpoint' for the Keycloak realm. /// - public string UserInfoEndpoint { get; set; } = "protocol/openid-connect/userinfo"; + public string UserInfoEndpoint => "protocol/openid-connect/userinfo"; /// /// Gets 'user_info_endpoint' expression for the Keycloak realm. @@ -78,7 +102,7 @@ public sealed class KeycloakRealmResource(string name, string realmName, Keycloa /// /// Gets or sets the 'end_session_endpoint' for the Keycloak realm. /// - public string EndSessionEndpoint { get; set; } = "protocol/openid-connect/logout"; + public string EndSessionEndpoint => "protocol/openid-connect/logout"; /// /// Gets the 'end_session_endpoint' expression for the Keycloak realm. @@ -88,7 +112,7 @@ public sealed class KeycloakRealmResource(string name, string realmName, Keycloa /// /// Gets or sets the 'registration_endpoint' for the Keycloak realm. /// - public string RegistrationEndpoint { get; set; } = "clients-registrations/openid-connect"; + public string RegistrationEndpoint => "clients-registrations/openid-connect"; /// /// Gets the 'registration_endpoint' expression for the Keycloak realm. @@ -96,10 +120,10 @@ public sealed class KeycloakRealmResource(string name, string realmName, Keycloa public ReferenceExpression RegistrationEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{RegistrationEndpoint}"); /// - public KeycloakResource Parent { get; } = parent; + public KeycloakResource Parent { get; } /// /// Gets the name of the realm. /// - public string RealmName { get; } = realmName; + public string RealmName { get; } } diff --git a/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs b/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs index cb3c701faf..4e90e55f65 100644 --- a/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs +++ b/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs @@ -34,6 +34,49 @@ public void CtorKeycloakResourceShouldThrowWhenAdminPasswordIsNull() Assert.Equal(nameof(adminPassword), exception.ParamName); } + [Fact] + public void CtorKeycloakRealmResourceShouldThrowWhenNameIsNull() + { + string name = null!; + var realmName = "realm1"; + var builder = TestDistributedApplicationBuilder.Create(); + var adminPassword = builder.AddParameter("Password"); + var parent = new KeycloakResource("keycloak", default(ParameterResource?), adminPassword.Resource); + + var action = () => new KeycloakRealmResource(name, realmName, parent); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } + + [Fact] + public void CtorMongoKeycloakRealmResourceShouldThrowWhenRealmNameIsNull() + { + var name = "keycloak"; + string realmName = null!; + var builder = TestDistributedApplicationBuilder.Create(); + var adminPassword = builder.AddParameter("Password"); + var parent = new KeycloakResource("keycloak", default(ParameterResource?), adminPassword.Resource); + + var action = () => new KeycloakRealmResource(name, realmName, parent); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(realmName), exception.ParamName); + } + + [Fact] + public void CtorMongoKeycloakRealmResourceShouldThrowWhenDatabaseParentIsNull() + { + var name = "keycloak"; + var realmName = "realm1"; + KeycloakResource parent = null!; + + var action = () => new KeycloakRealmResource(name, realmName, parent); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(parent), exception.ParamName); + } + [Fact] public void AddKeycloakContainerShouldThrowWhenBuilderIsNull() { @@ -195,4 +238,29 @@ public void WithRealmImportFileAddsBindMountAnnotation(bool? isReadOnly) Assert.Equal(ContainerMountType.BindMount, containerAnnotation.Type); Assert.Equal(isReadOnly ?? false, containerAnnotation.IsReadOnly); } + + [Fact] + public void AddRealmShouldThrowWhenBuilderIsNull() + { + IResourceBuilder builder = null!; + const string name = "realm1"; + + var action = () => builder.AddRealm(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddRealmShouldThrowWhenNameIsNull() + { + var builderResource = TestDistributedApplicationBuilder.Create(); + var MongoDB = builderResource.AddKeycloak("realm1"); + string name = null!; + + var action = () => MongoDB.AddRealm(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } }