diff --git a/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs new file mode 100644 index 0000000000..6070e77ad7 --- /dev/null +++ b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs @@ -0,0 +1,129 @@ +// 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. +/// +public sealed class KeycloakRealmResource : Resource, IResourceWithParent, IResourceWithConnectionString +{ + private EndpointReference? _parentEndpoint; + 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($"{ParentUrl}/{RealmPath}/"); + + /// + /// Gets the base address of the realm. + /// + public string RealmPath { get; } + + /// + /// Gets the issuer expression for the Keycloak realm. + /// + public ReferenceExpression IssuerUrlExpression => ReferenceExpression.Create($"{ParentUrl}/{RealmPath}"); + + /// + /// Gets or sets the metadata address for the Keycloak realm. + /// + public string MetadataAddress => ".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 => "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 => "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 => "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 => "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 => "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 => "clients-registrations/openid-connect"; + + /// + /// Gets the 'registration_endpoint' expression for the Keycloak realm. + /// + public ReferenceExpression RegistrationEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{RegistrationEndpoint}"); + + /// + public KeycloakResource Parent { get; } + + /// + /// Gets the name of the realm. + /// + public string RealmName { get; } +} 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); + } } 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); + } }